<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Di\ServiceLocator; use Zend\Code\Generator\ClassGenerator; use Zend\Code\Generator\FileGenerator; use Zend\Code\Generator\MethodGenerator; use Zend\Code\Generator\ParameterGenerator; use Zend\Di\Di; use Zend\Di\Exception; /** * Generator that creates the body of a service locator that can emulate the logic of the given Zend\Di\Di instance * without class definitions */ class Generator { protected $containerClass = 'ApplicationContext'; /** @var DependencyInjectorProxy */ protected $injector; /** * @var null|string */ protected $namespace; /** * Constructor * * Requires a DependencyInjection manager on which to operate. * * @param Di $injector */ public function __construct(Di $injector) { $this->injector = new DependencyInjectorProxy($injector); } /** * Set the class name for the generated service locator container * * @param string $name * @return Generator */ public function setContainerClass($name) { $this->containerClass = $name; return $this; } /** * Set the namespace to use for the generated class file * * @param string $namespace * @return Generator */ public function setNamespace($namespace) { $this->namespace = $namespace; return $this; } /** * Construct, configure, and return a PHP class file code generation object * * Creates a Zend\Code\Generator\FileGenerator object that has * created the specified class and service locator methods. * * @param null|string $filename * @throws \Zend\Di\Exception\RuntimeException * @return FileGenerator */ public function getCodeGenerator($filename = null) { $injector = $this->injector; $im = $injector->instanceManager(); $indent = ' '; $aliases = $this->reduceAliases($im->getAliases()); $caseStatements = array(); $getters = array(); $definitions = $injector->definitions(); $fetched = array_unique(array_merge($definitions->getClasses(), $im->getAliases())); foreach ($fetched as $name) { $getter = $this->normalizeAlias($name); $meta = $injector->get($name); $params = $meta->getParams(); // Build parameter list for instantiation foreach ($params as $key => $param) { if (null === $param || is_scalar($param) || is_array($param)) { $string = var_export($param, 1); if (strstr($string, '::__set_state(')) { throw new Exception\RuntimeException('Arguments in definitions may not contain objects'); } $params[$key] = $string; } elseif ($param instanceof GeneratorInstance) { /* @var $param GeneratorInstance */ $params[$key] = sprintf('$this->%s()', $this->normalizeAlias($param->getName())); } else { $message = sprintf('Unable to use object arguments when building containers. Encountered with "%s", parameter of type "%s"', $name, get_class($param)); throw new Exception\RuntimeException($message); } } // Strip null arguments from the end of the params list $reverseParams = array_reverse($params, true); foreach ($reverseParams as $key => $param) { if ('NULL' === $param) { unset($params[$key]); continue; } break; } // Create instantiation code $constructor = $meta->getConstructor(); if ('__construct' != $constructor) { // Constructor callback $callback = var_export($constructor, 1); if (strstr($callback, '::__set_state(')) { throw new Exception\RuntimeException('Unable to build containers that use callbacks requiring object instances'); } if (count($params)) { $creation = sprintf('$object = call_user_func(%s, %s);', $callback, implode(', ', $params)); } else { $creation = sprintf('$object = call_user_func(%s);', $callback); } } else { // Normal instantiation $className = '\\' . ltrim($name, '\\'); $creation = sprintf('$object = new %s(%s);', $className, implode(', ', $params)); } // Create method call code $methods = ''; foreach ($meta->getMethods() as $methodData) { if (!isset($methodData['name']) && !isset($methodData['method'])) { continue; } $methodName = isset($methodData['name']) ? $methodData['name'] : $methodData['method']; $methodParams = $methodData['params']; // Create method parameter representation foreach ($methodParams as $key => $param) { if (null === $param || is_scalar($param) || is_array($param)) { $string = var_export($param, 1); if (strstr($string, '::__set_state(')) { throw new Exception\RuntimeException('Arguments in definitions may not contain objects'); } $methodParams[$key] = $string; } elseif ($param instanceof GeneratorInstance) { $methodParams[$key] = sprintf('$this->%s()', $this->normalizeAlias($param->getName())); } else { $message = sprintf('Unable to use object arguments when generating method calls. Encountered with class "%s", method "%s", parameter of type "%s"', $name, $methodName, get_class($param)); throw new Exception\RuntimeException($message); } } // Strip null arguments from the end of the params list $reverseParams = array_reverse($methodParams, true); foreach ($reverseParams as $key => $param) { if ('NULL' === $param) { unset($methodParams[$key]); continue; } break; } $methods .= sprintf("\$object->%s(%s);\n", $methodName, implode(', ', $methodParams)); } // Generate caching statement $storage = ''; if ($im->hasSharedInstance($name, $params)) { $storage = sprintf("\$this->services['%s'] = \$object;\n", $name); } // Start creating getter $getterBody = ''; // Create fetch of stored service if ($im->hasSharedInstance($name, $params)) { $getterBody .= sprintf("if (isset(\$this->services['%s'])) {\n", $name); $getterBody .= sprintf("%sreturn \$this->services['%s'];\n}\n\n", $indent, $name); } // Creation and method calls $getterBody .= sprintf("%s\n", $creation); $getterBody .= $methods; // Stored service $getterBody .= $storage; // End getter body $getterBody .= "return \$object;\n"; $getterDef = new MethodGenerator(); $getterDef->setName($getter); $getterDef->setBody($getterBody); $getters[] = $getterDef; // Get cases for case statements $cases = array($name); if (isset($aliases[$name])) { $cases = array_merge($aliases[$name], $cases); } // Build case statement and store $statement = ''; foreach ($cases as $value) { $statement .= sprintf("%scase '%s':\n", $indent, $value); } $statement .= sprintf("%sreturn \$this->%s();\n", str_repeat($indent, 2), $getter); $caseStatements[] = $statement; } // Build switch statement $switch = sprintf("switch (%s) {\n%s\n", '$name', implode("\n", $caseStatements)); $switch .= sprintf("%sdefault:\n%sreturn parent::get(%s, %s);\n", $indent, str_repeat($indent, 2), '$name', '$params'); $switch .= "}\n\n"; // Build get() method $nameParam = new ParameterGenerator(); $nameParam->setName('name'); $paramsParam = new ParameterGenerator(); $paramsParam->setName('params') ->setType('array') ->setDefaultValue(array()); $get = new MethodGenerator(); $get->setName('get'); $get->setParameters(array( $nameParam, $paramsParam, )); $get->setBody($switch); // Create getters for aliases $aliasMethods = array(); foreach ($aliases as $class => $classAliases) { foreach ($classAliases as $alias) { $aliasMethods[] = $this->getCodeGenMethodFromAlias($alias, $class); } } // Create class code generation object $container = new ClassGenerator(); $container->setName($this->containerClass) ->setExtendedClass('ServiceLocator') ->addMethodFromGenerator($get) ->addMethods($getters) ->addMethods($aliasMethods); // Create PHP file code generation object $classFile = new FileGenerator(); $classFile->setUse('Zend\Di\ServiceLocator') ->setClass($container); if (null !== $this->namespace) { $classFile->setNamespace($this->namespace); } if (null !== $filename) { $classFile->setFilename($filename); } return $classFile; } /** * Reduces aliases * * Takes alias list and reduces it to a 2-dimensional array of * class names pointing to an array of aliases that resolve to * it. * * @param array $aliasList * @return array */ protected function reduceAliases(array $aliasList) { $reduced = array(); $aliases = array_keys($aliasList); foreach ($aliasList as $alias => $service) { if (in_array($service, $aliases)) { do { $service = $aliasList[$service]; } while (in_array($service, $aliases)); } if (!isset($reduced[$service])) { $reduced[$service] = array(); } $reduced[$service][] = $alias; } return $reduced; } /** * Create a PhpMethod code generation object named after a given alias * * @param string $alias * @param string $class Class to which alias refers * @return MethodGenerator */ protected function getCodeGenMethodFromAlias($alias, $class) { $alias = $this->normalizeAlias($alias); $method = new MethodGenerator(); $method->setName($alias); $method->setBody(sprintf('return $this->get(\'%s\');', $class)); return $method; } /** * Normalize an alias to a getter method name * * @param string $alias * @return string */ protected function normalizeAlias($alias) { $normalized = preg_replace('/[^a-zA-Z0-9]/', ' ', $alias); $normalized = 'get' . str_replace(' ', '', ucwords($normalized)); return $normalized; } }