PageRenderTime 27ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

https://github.com/Exercise/symfony
PHP | 505 lines | 311 code | 63 blank | 131 comment | 46 complexity | 3364f8dd3c6c59793e279795bb792b3a MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\DependencyInjection\Loader;
  11. use Symfony\Component\DependencyInjection\DefinitionDecorator;
  12. use Symfony\Component\DependencyInjection\ContainerInterface;
  13. use Symfony\Component\DependencyInjection\Alias;
  14. use Symfony\Component\DependencyInjection\Definition;
  15. use Symfony\Component\DependencyInjection\Reference;
  16. use Symfony\Component\DependencyInjection\SimpleXMLElement;
  17. use Symfony\Component\Config\Resource\FileResource;
  18. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  19. use Symfony\Component\DependencyInjection\Exception\RuntimeException;
  20. /**
  21. * XmlFileLoader loads XML files service definitions.
  22. *
  23. * @author Fabien Potencier <fabien@symfony.com>
  24. */
  25. class XmlFileLoader extends FileLoader
  26. {
  27. /**
  28. * Loads an XML file.
  29. *
  30. * @param mixed $file The resource
  31. * @param string $type The resource type
  32. */
  33. public function load($file, $type = null)
  34. {
  35. $path = $this->locator->locate($file);
  36. $xml = $this->parseFile($path);
  37. $xml->registerXPathNamespace('container', 'http://symfony.com/schema/dic/services');
  38. $this->container->addResource(new FileResource($path));
  39. // anonymous services
  40. $xml = $this->processAnonymousServices($xml, $path);
  41. // imports
  42. $this->parseImports($xml, $path);
  43. // parameters
  44. $this->parseParameters($xml, $path);
  45. // extensions
  46. $this->loadFromExtensions($xml);
  47. // services
  48. $this->parseDefinitions($xml, $path);
  49. }
  50. /**
  51. * Returns true if this class supports the given resource.
  52. *
  53. * @param mixed $resource A resource
  54. * @param string $type The resource type
  55. *
  56. * @return Boolean true if this class supports the given resource, false otherwise
  57. */
  58. public function supports($resource, $type = null)
  59. {
  60. return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION);
  61. }
  62. /**
  63. * Parses parameters
  64. *
  65. * @param SimpleXMLElement $xml
  66. * @param string $file
  67. */
  68. private function parseParameters(SimpleXMLElement $xml, $file)
  69. {
  70. if (!$xml->parameters) {
  71. return;
  72. }
  73. $this->container->getParameterBag()->add($xml->parameters->getArgumentsAsPhp('parameter'));
  74. }
  75. /**
  76. * Parses imports
  77. *
  78. * @param SimpleXMLElement $xml
  79. * @param string $file
  80. */
  81. private function parseImports(SimpleXMLElement $xml, $file)
  82. {
  83. if (false === $imports = $xml->xpath('//container:imports/container:import')) {
  84. return;
  85. }
  86. foreach ($imports as $import) {
  87. $this->setCurrentDir(dirname($file));
  88. $this->import((string) $import['resource'], null, (Boolean) $import->getAttributeAsPhp('ignore-errors'), $file);
  89. }
  90. }
  91. /**
  92. * Parses multiple definitions
  93. *
  94. * @param SimpleXMLElement $xml
  95. * @param string $file
  96. */
  97. private function parseDefinitions(SimpleXMLElement $xml, $file)
  98. {
  99. if (false === $services = $xml->xpath('//container:services/container:service')) {
  100. return;
  101. }
  102. foreach ($services as $service) {
  103. $this->parseDefinition((string) $service['id'], $service, $file);
  104. }
  105. }
  106. /**
  107. * Parses an individual Definition
  108. *
  109. * @param string $id
  110. * @param SimpleXMLElement $service
  111. * @param string $file
  112. */
  113. private function parseDefinition($id, $service, $file)
  114. {
  115. if ((string) $service['alias']) {
  116. $public = true;
  117. if (isset($service['public'])) {
  118. $public = $service->getAttributeAsPhp('public');
  119. }
  120. $this->container->setAlias($id, new Alias((string) $service['alias'], $public));
  121. return;
  122. }
  123. if (isset($service['parent'])) {
  124. $definition = new DefinitionDecorator((string) $service['parent']);
  125. } else {
  126. $definition = new Definition();
  127. }
  128. foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'abstract') as $key) {
  129. if (isset($service[$key])) {
  130. $method = 'set'.str_replace('-', '', $key);
  131. $definition->$method((string) $service->getAttributeAsPhp($key));
  132. }
  133. }
  134. if ($service->file) {
  135. $definition->setFile((string) $service->file);
  136. }
  137. $definition->setArguments($service->getArgumentsAsPhp('argument'));
  138. $definition->setProperties($service->getArgumentsAsPhp('property'));
  139. if (isset($service->configurator)) {
  140. if (isset($service->configurator['function'])) {
  141. $definition->setConfigurator((string) $service->configurator['function']);
  142. } else {
  143. if (isset($service->configurator['service'])) {
  144. $class = new Reference((string) $service->configurator['service'], ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
  145. } else {
  146. $class = (string) $service->configurator['class'];
  147. }
  148. $definition->setConfigurator(array($class, (string) $service->configurator['method']));
  149. }
  150. }
  151. foreach ($service->call as $call) {
  152. $definition->addMethodCall((string) $call['method'], $call->getArgumentsAsPhp('argument'));
  153. }
  154. foreach ($service->tag as $tag) {
  155. $parameters = array();
  156. foreach ($tag->attributes() as $name => $value) {
  157. if ('name' === $name) {
  158. continue;
  159. }
  160. $parameters[$name] = SimpleXMLElement::phpize($value);
  161. }
  162. $definition->addTag((string) $tag['name'], $parameters);
  163. }
  164. $this->container->setDefinition($id, $definition);
  165. }
  166. /**
  167. * Parses a XML file.
  168. *
  169. * @param string $file Path to a file
  170. *
  171. * @throws InvalidArgumentException When loading of XML file returns error
  172. */
  173. private function parseFile($file)
  174. {
  175. $dom = new \DOMDocument();
  176. libxml_use_internal_errors(true);
  177. if (!$dom->load($file, defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0)) {
  178. throw new InvalidArgumentException(implode("\n", $this->getXmlErrors()));
  179. }
  180. $dom->validateOnParse = true;
  181. $dom->normalizeDocument();
  182. libxml_use_internal_errors(false);
  183. $this->validate($dom, $file);
  184. return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement');
  185. }
  186. /**
  187. * Processes anonymous services
  188. *
  189. * @param SimpleXMLElement $xml
  190. * @param string $file
  191. *
  192. * @return array An array of anonymous services
  193. */
  194. private function processAnonymousServices(SimpleXMLElement $xml, $file)
  195. {
  196. $definitions = array();
  197. $count = 0;
  198. // anonymous services as arguments
  199. if (false === $nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]')) {
  200. return $xml;
  201. }
  202. foreach ($nodes as $node) {
  203. // give it a unique name
  204. $node['id'] = sprintf('%s_%d', md5($file), ++$count);
  205. $definitions[(string) $node['id']] = array($node->service, $file, false);
  206. $node->service['id'] = (string) $node['id'];
  207. }
  208. // anonymous services "in the wild"
  209. if (false === $nodes = $xml->xpath('//container:services/container:service[not(@id)]')) {
  210. return $xml;
  211. }
  212. foreach ($nodes as $node) {
  213. // give it a unique name
  214. $node['id'] = sprintf('%s_%d', md5($file), ++$count);
  215. $definitions[(string) $node['id']] = array($node, $file, true);
  216. $node->service['id'] = (string) $node['id'];
  217. }
  218. // resolve definitions
  219. krsort($definitions);
  220. foreach ($definitions as $id => $def) {
  221. // anonymous services are always private
  222. $def[0]['public'] = false;
  223. $this->parseDefinition($id, $def[0], $def[1]);
  224. $oNode = dom_import_simplexml($def[0]);
  225. if (true === $def[2]) {
  226. $nNode = new \DOMElement('_services');
  227. $oNode->parentNode->replaceChild($nNode, $oNode);
  228. $nNode->setAttribute('id', $id);
  229. } else {
  230. $oNode->parentNode->removeChild($oNode);
  231. }
  232. }
  233. return $xml;
  234. }
  235. /**
  236. * Validates an XML document.
  237. *
  238. * @param DOMDocument $dom
  239. * @param string $file
  240. */
  241. private function validate(\DOMDocument $dom, $file)
  242. {
  243. $this->validateSchema($dom, $file);
  244. $this->validateExtensions($dom, $file);
  245. }
  246. /**
  247. * Validates a documents XML schema.
  248. *
  249. * @param \DOMDocument $dom
  250. * @param string $file
  251. *
  252. * @throws RuntimeException When extension references a non-existent XSD file
  253. * @throws InvalidArgumentException When XML doesn't validate its XSD schema
  254. */
  255. private function validateSchema(\DOMDocument $dom, $file)
  256. {
  257. $schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
  258. if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
  259. $items = preg_split('/\s+/', $element);
  260. for ($i = 0, $nb = count($items); $i < $nb; $i += 2) {
  261. if (!$this->container->hasExtension($items[$i])) {
  262. continue;
  263. }
  264. if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
  265. $path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
  266. if (!is_file($path)) {
  267. throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path));
  268. }
  269. $schemaLocations[$items[$i]] = $path;
  270. }
  271. }
  272. }
  273. $tmpfiles = array();
  274. $imports = '';
  275. foreach ($schemaLocations as $namespace => $location) {
  276. $parts = explode('/', $location);
  277. if (0 === stripos($location, 'phar://')) {
  278. $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
  279. if ($tmpfile) {
  280. copy($location, $tmpfile);
  281. $tmpfiles[] = $tmpfile;
  282. $parts = explode('/', str_replace('\\', '/', $tmpfile));
  283. }
  284. }
  285. $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
  286. $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
  287. $imports .= sprintf(' <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
  288. }
  289. $source = <<<EOF
  290. <?xml version="1.0" encoding="utf-8" ?>
  291. <xsd:schema xmlns="http://symfony.com/schema"
  292. xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  293. targetNamespace="http://symfony.com/schema"
  294. elementFormDefault="qualified">
  295. <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
  296. $imports
  297. </xsd:schema>
  298. EOF
  299. ;
  300. $current = libxml_use_internal_errors(true);
  301. $valid = $dom->schemaValidateSource($source);
  302. foreach ($tmpfiles as $tmpfile) {
  303. @unlink($tmpfile);
  304. }
  305. if (!$valid) {
  306. throw new InvalidArgumentException(implode("\n", $this->getXmlErrors()));
  307. }
  308. libxml_use_internal_errors($current);
  309. }
  310. /**
  311. * Validates an extension.
  312. *
  313. * @param \DOMDocument $dom
  314. * @param string $file
  315. *
  316. * @throws InvalidArgumentException When no extension is found corresponding to a tag
  317. */
  318. private function validateExtensions(\DOMDocument $dom, $file)
  319. {
  320. foreach ($dom->documentElement->childNodes as $node) {
  321. if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
  322. continue;
  323. }
  324. // can it be handled by an extension?
  325. if (!$this->container->hasExtension($node->namespaceURI)) {
  326. $extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
  327. throw new InvalidArgumentException(sprintf(
  328. 'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
  329. $node->tagName,
  330. $file,
  331. $node->namespaceURI,
  332. $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
  333. ));
  334. }
  335. }
  336. }
  337. /**
  338. * Returns an array of XML errors.
  339. *
  340. * @return array
  341. */
  342. private function getXmlErrors()
  343. {
  344. $errors = array();
  345. foreach (libxml_get_errors() as $error) {
  346. $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
  347. LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
  348. $error->code,
  349. trim($error->message),
  350. $error->file ? $error->file : 'n/a',
  351. $error->line,
  352. $error->column
  353. );
  354. }
  355. libxml_clear_errors();
  356. return $errors;
  357. }
  358. /**
  359. * Loads from an extension.
  360. *
  361. * @param SimpleXMLElement $xml
  362. */
  363. private function loadFromExtensions(SimpleXMLElement $xml)
  364. {
  365. foreach (dom_import_simplexml($xml)->childNodes as $node) {
  366. if (!$node instanceof \DOMElement || $node->namespaceURI === 'http://symfony.com/schema/dic/services') {
  367. continue;
  368. }
  369. $values = static::convertDomElementToArray($node);
  370. if (!is_array($values)) {
  371. $values = array();
  372. }
  373. $this->container->loadFromExtension($node->namespaceURI, $values);
  374. }
  375. }
  376. /**
  377. * Converts a \DomElement object to a PHP array.
  378. *
  379. * The following rules applies during the conversion:
  380. *
  381. * * Each tag is converted to a key value or an array
  382. * if there is more than one "value"
  383. *
  384. * * The content of a tag is set under a "value" key (<foo>bar</foo>)
  385. * if the tag also has some nested tags
  386. *
  387. * * The attributes are converted to keys (<foo foo="bar"/>)
  388. *
  389. * * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
  390. *
  391. * @param \DomElement $element A \DomElement instance
  392. *
  393. * @return array A PHP array
  394. */
  395. static public function convertDomElementToArray(\DomElement $element)
  396. {
  397. $empty = true;
  398. $config = array();
  399. foreach ($element->attributes as $name => $node) {
  400. $config[$name] = SimpleXMLElement::phpize($node->value);
  401. $empty = false;
  402. }
  403. $nodeValue = false;
  404. foreach ($element->childNodes as $node) {
  405. if ($node instanceof \DOMText) {
  406. if (trim($node->nodeValue)) {
  407. $nodeValue = trim($node->nodeValue);
  408. $empty = false;
  409. }
  410. } elseif (!$node instanceof \DOMComment) {
  411. if ($node instanceof \DOMElement && '_services' === $node->nodeName) {
  412. $value = new Reference($node->getAttribute('id'));
  413. } else {
  414. $value = static::convertDomElementToArray($node);
  415. }
  416. $key = $node->localName;
  417. if (isset($config[$key])) {
  418. if (!is_array($config[$key]) || !is_int(key($config[$key]))) {
  419. $config[$key] = array($config[$key]);
  420. }
  421. $config[$key][] = $value;
  422. } else {
  423. $config[$key] = $value;
  424. }
  425. $empty = false;
  426. }
  427. }
  428. if (false !== $nodeValue) {
  429. $value = SimpleXMLElement::phpize($nodeValue);
  430. if (count($config)) {
  431. $config['value'] = $value;
  432. } else {
  433. $config = $value;
  434. }
  435. }
  436. return !$empty ? $config : null;
  437. }
  438. }