From bd320533ab85e45333ad1222a37787c52119bed8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 30 Jan 2013 19:49:38 -0500 Subject: [PATCH] MVC WIP --- framework/YiiBase.php | 19 +-- framework/base/Controller.php | 11 ++ framework/base/ErrorHandler.php | 2 +- framework/base/Theme.php | 105 +++++++++++--- framework/base/View.php | 302 +++++++++++++++++++--------------------- framework/base/Widget.php | 12 ++ framework/util/FileHelper.php | 14 ++ 7 files changed, 272 insertions(+), 193 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 1230053..14e0f44 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -8,9 +8,8 @@ */ use yii\base\Exception; -use yii\logging\Logger; -use yii\base\InvalidCallException; use yii\base\InvalidConfigException; +use yii\logging\Logger; /** * Gets the application start timestamp. @@ -189,14 +188,14 @@ class YiiBase * * Note, this method does not ensure the existence of the resulting path. * @param string $alias alias - * @param boolean $throwException whether to throw exception if the alias is invalid. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. - * @throws Exception if the alias is invalid and $throwException is true. * @see setAlias */ - public static function getAlias($alias, $throwException = false) + public static function getAlias($alias) { - if (isset(self::$aliases[$alias])) { + if (!is_string($alias)) { + return false; + } elseif (isset(self::$aliases[$alias])) { return self::$aliases[$alias]; } elseif ($alias === '' || $alias[0] !== '@') { // not an alias return $alias; @@ -206,11 +205,7 @@ class YiiBase return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); } } - if ($throwException) { - throw new Exception("Invalid path alias: $alias"); - } else { - return false; - } + return false; } /** @@ -361,7 +356,7 @@ class YiiBase $class = $config['class']; unset($config['class']); } else { - throw new InvalidCallException('Object configuration must be an array containing a "class" element.'); + throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); } if (!class_exists($class, false)) { diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 5b7b460..d8cfbd2 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -279,4 +279,15 @@ class Controller extends Component { return new View($this); } + + /** + * Returns the directory containing view files for this controller. + * The default implementation returns the directory named as controller [[id]] under the [[module]]'s + * [[viewPath]] directory. + * @return string the directory containing the view files for this controller. + */ + public function getViewPath() + { + return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; + } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index c17755e..23b5d57 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -321,7 +321,7 @@ class ErrorHandler extends Component public function renderAsHtml($exception) { $view = new View; - $view->context = $this; + $view->_owner = $this; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; echo $view->render($name, array( 'exception' => $exception, diff --git a/framework/base/Theme.php b/framework/base/Theme.php index c1fc94a..03f8f55 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -9,55 +9,114 @@ namespace yii\base; +use Yii; use yii\base\InvalidConfigException; +use yii\util\FileHelper; /** * Theme represents an application theme. * + * A theme is directory consisting of view and layout files which are meant to replace their + * non-themed counterparts. + * + * Theme uses [[pathMap]] to achieve the file replacement. A view or layout file will be replaced + * with its themed version if part of its path matches one of the keys in [[pathMap]]. + * Then the matched part will be replaced with the corresponding array value. + * + * For example, if [[pathMap]] is `array('/www/views' => '/www/themes/basic')`, + * then the themed version for a view file `/www/views/site/index.php` will be + * `/www/themes/basic/site/index.php`. + * + * @property string $baseUrl the base URL for this theme. This is mainly used by [[getUrl()]]. + * * @author Qiang Xue * @since 2.0 */ class Theme extends Component { + /** + * @var string the root path of this theme. + * @see pathMap + */ public $basePath; - public $baseUrl; + /** + * @var array the mapping between view directories and their corresponding themed versions. + * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. + * This property is used by [[apply()]] when a view is trying to apply the theme. + */ + public $pathMap; + + private $_baseUrl; + /** + * Initializes the theme. + * @throws InvalidConfigException if [[basePath]] is not set. + */ public function init() { - if ($this->basePath !== null) { - $this->basePath = \Yii::getAlias($this->basePath, true); - } else { - throw new InvalidConfigException("Theme.basePath must be set."); + parent::init(); + if (empty($this->pathMap)) { + if ($this->basePath !== null) { + $this->basePath = FileHelper::ensureDirectory($this->basePath); + $this->pathMap = array(Yii::$application->getBasePath() => $this->basePath); + } else { + throw new InvalidConfigException("Theme::basePath must be set."); + } } - if ($this->baseUrl !== null) { - $this->baseUrl = \Yii::getAlias($this->baseUrl, true); - } else { - throw new InvalidConfigException("Theme.baseUrl must be set."); + $paths = array(); + foreach ($this->pathMap as $from => $to) { + $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR; } + $this->pathMap = $paths; + } + + /** + * Returns the base URL for this theme. + * The method [[getUrl()]] will prefix this to the given URL. + * @return string the base URL for this theme. + */ + public function getBaseUrl() + { + return $this->_baseUrl; + } + + /** + * Sets the base URL for this theme. + * @param string $value the base URL for this theme. + */ + public function setBaseUrl($value) + { + $this->_baseUrl = rtrim(Yii::getAlias($value), '/'); } /** - * @param Application|Module|Controller|Object $context - * @return string + * Converts a file to a themed file if possible. + * If there is no corresponding themed file, the original file will be returned. + * @param string $path the file to be themed + * @return string the themed file, or the original file if the themed version is not available. */ - public function getViewPath($context = null) + public function apply($path) { - $viewPath = $this->basePath . DIRECTORY_SEPARATOR . 'views'; - if ($context === null || $context instanceof Application) { - return $viewPath; - } elseif ($context instanceof Controller || $context instanceof Module) { - return $viewPath . DIRECTORY_SEPARATOR . $context->getUniqueId(); - } else { - return $viewPath . DIRECTORY_SEPARATOR . str_replace('\\', '_', get_class($context)); + $path = FileHelper::normalizePath($path); + foreach ($this->pathMap as $from => $to) { + if (strpos($path, $from) === 0) { + $n = strlen($from); + $file = $to . substr($path, $n); + if (is_file($file)) { + return $file; + } + } } + return $path; } /** - * @param Module $module - * @return string + * Converts a relative URL into an absolute URL using [[basePath]]. + * @param string $url the relative URL to be converted. + * @return string the absolute URL */ - public function getLayoutPath($module = null) + public function getUrl($url) { - return $this->getViewPath($module) . DIRECTORY_SEPARATOR . 'layouts'; + return $this->baseUrl . '/' . ltrim($url, '/'); } } diff --git a/framework/base/View.php b/framework/base/View.php index 9657025..f4dd07b 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -9,27 +9,26 @@ namespace yii\base; +use Yii; use yii\util\FileHelper; use yii\base\Application; /** + * View represents a view object in the MVC pattern. + * + * View provides a set of methods (e.g. [[render()]]) for rendering purpose. + * + * * @author Qiang Xue * @since 2.0 */ class View extends Component { /** - * @var Controller|Widget|Object the context under which this view is being rendered + * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called. + * If not set, it will use the value of [[Application::layout]]. */ - public $context; - /** - * @var string|array the directories where the view file should be looked for when a *relative* view name is given. - * This can be either a string representing a single directory, or an array representing multiple directories. - * If the latter, the view file will be looked for in the directories in the order they are specified. - * Path aliases can be used. If this property is not set, relative view names should be treated as absolute ones. - * @see roothPath - */ - public $basePath; + public $layout; /** * @var string the language that the view should be rendered in. If not set, it will use * the value of [[Application::language]]. @@ -45,45 +44,76 @@ class View extends Component * Note that when this is true, if a localized view cannot be found, the original view will be rendered. * No error will be reported. */ - public $localizeView = true; + public $enableI18N = true; /** * @var boolean whether to theme the view when possible. Defaults to true. - * Note that theming will be disabled if [[Application::theme]] is null. + * Note that theming will be disabled if [[Application::theme]] is not set. */ - public $themeView = true; + public $enableTheme = true; /** * @var mixed custom parameters that are available in the view template */ public $params; + + /** + * @var object the object that owns this view. + */ + private $_owner; /** * @var Widget[] the widgets that are currently not ended */ - protected $widgetStack = array(); + private $_widgetStack = array(); /** * Constructor. - * @param Controller|Widget|Object $context the context under which this view is being rendered (e.g. controller, widget) + * @param object $owner the owner of this view. This usually is a controller or a widget. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($context = null, $config = array()) + public function __construct($owner, $config = array()) { - $this->context = $context; + $this->_owner = $owner; parent::__construct($config); } + /** + * Returns the owner of this view. + * @return object the owner of this view. + */ + public function getOwner() + { + return $this->_owner; + } + + /** + * Renders a view within the layout specified by [[owner]]. + * This method is similar to [[renderPartial()]] except that if [[owner]] specifies a layout, + * this method will embed the view result into the layout and then return it. + * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[searchPaths]]. + * @param array $params the parameters that should be made available in the view. The PHP function `extract()` + * will be called on this variable to extract the variables from this parameter. + * @return string the rendering result + * @throws InvalidCallException if the view file cannot be found + * @see renderPartial() + */ public function render($view, $params = array()) { $content = $this->renderPartial($view, $params); - return $this->renderText($content); + return $this->renderContent($content); } - public function renderText($text) + /** + * Renders a text content within the layout specified by [[owner]]. + * If the [[owner]] does not specify any layout, the content will be returned back. + * @param string $content the content to be rendered + * @return string the rendering result + */ + public function renderContent($content) { $layoutFile = $this->findLayoutFile(); if ($layoutFile !== false) { - return $this->renderFile($layoutFile, array('content' => $text)); + return $this->renderFile($layoutFile, array('content' => $content)); } else { - return $text; + return $content; } } @@ -94,18 +124,16 @@ class View extends Component * It then calls [[renderFile()]] to render the view file. The rendering result is returned * as a string. If the view file does not exist, an exception will be thrown. * - * To determine which view file should be rendered, the method calls [[findViewFile()]] which - * will search in the directories as specified by [[basePath]]. - * * View name can be a path alias representing an absolute file path (e.g. `@application/views/layout/index`), - * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given + * or a path relative to [[searchPaths]]. The file suffix is optional and defaults to `.php` if not given * in the view name. * - * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. + * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[searchPaths]]. * @param array $params the parameters that should be made available in the view. The PHP function `extract()` * will be called on this variable to extract the variables from this parameter. * @return string the rendering result * @throws InvalidCallException if the view file cannot be found + * @see findViewFile() */ public function renderPartial($view, $params = array()) { @@ -119,19 +147,25 @@ class View extends Component /** * Renders a view file. - * @param string $file the view file path - * @param array $params the parameters to be extracted and made available in the view file + * This method will extract the given parameters and include the view file. + * It captures the output of the included view file and returns it as a string. + * @param string $_file_ the view file. + * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result */ - public function renderFile($file, $params = array()) + public function renderFile($_file_, $_params_ = array()) { - return $this->renderFileInternal($file, $params); + ob_start(); + ob_implicit_flush(false); + extract($_params_, EXTR_OVERWRITE); + require($_file_); + return ob_get_clean(); } public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return \Yii::createObject($properties, $this->context); + return Yii::createObject($properties, $this->_owner); } public function widget($class, $properties = array(), $captureOutput = false) @@ -158,7 +192,7 @@ class View extends Component public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $this->widgetStack[] = $widget; + $this->_widgetStack[] = $widget; return $widget; } @@ -173,7 +207,7 @@ class View extends Component public function endWidget() { /** @var $widget Widget */ - if (($widget = array_pop($this->widgetStack)) !== null) { + if (($widget = array_pop($this->_widgetStack)) !== null) { $widget->run(); return $widget; } else { @@ -273,141 +307,75 @@ class View extends Component } /** - * Renders a view file. - * This method will extract the given parameters and include the view file. - * It captures the output of the included view file and returns it as a string. - * @param string $_file_ the view file. - * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. - * @return string the rendering result - */ - protected function renderFileInternal($_file_, $_params_ = array()) - { - ob_start(); - ob_implicit_flush(false); - extract($_params_, EXTR_OVERWRITE); - require($_file_); - return ob_get_clean(); - } - - /** * Finds the view file based on the given view name. + * + * The rule for searching for the view file is as follows: + * + * - If the view name is given as a path alias, return the actual path corresponding to the alias; + * - If the view name does NOT start with a slash: + * * If the view owner is a controller or widget, look for the view file under + * the controller or widget's view path (see [[Controller::viewPath]] and [[Widget::viewPath]]); + * * If the view owner is an object, look for the view file under the "views" sub-directory + * of the directory containing the object class file; + * * Otherwise, look for the view file under the application's [[Application::viewPath|view path]]. + * - If the view name starts with a single slash, look for the view file under the currently active + * module's [[Module::viewPath|view path]]; + * - If the view name starts with double slashes, look for the view file under the application's + * [[Application::viewPath|view path]]. + * + * If [[enableTheme]] is true and there is an active application them, the method will also + * attempt to use a themed version of the view file, when available. + * * @param string $view the view name or path alias. If the view name does not specify * the view file extension name, it will use `.php` as the extension name. - * @return string|boolean the view file if it exists. False if the view file cannot be found. + * @return string|boolean the view file path if it exists. False if the view file cannot be found. */ public function findViewFile($view) { - if (($extension = FileHelper::getExtension($view)) === '') { + if (FileHelper::getExtension($view) === '') { $view .= '.php'; } if (strncmp($view, '@', 1) === 0) { - $file = \Yii::getAlias($view); + // e.g. "@application/views/common" + $file = Yii::getAlias($view); } elseif (strncmp($view, '/', 1) !== 0) { - $file = $this->findRelativeViewFile($view); - } else { - $file = $this->findAbsoluteViewFile($view); - } - - if ($file === false || !is_file($file)) { - return false; - } elseif ($this->localizeView) { - return FileHelper::localize($file, $this->language, $this->sourceLanguage); - } else { - return $file; - } - } - - /** - * Finds the view file corresponding to the given relative view name. - * The method will look for the view file under a set of directories returned by [[resolveBasePath()]]. - * If no base path is given, the view will be treated as an absolute view and the result of - * [[findAbsoluteViewFile()]] will be returned. - * @param string $view the relative view name - * @return string|boolean the view file path, or false if the view file cannot be found - */ - protected function findRelativeViewFile($view) - { - $paths = $this->resolveBasePath(); - if ($paths === array()) { - return $this->findAbsoluteViewFile($view); - } - if ($this->themeView && $this->context !== null && ($theme = \Yii::$application->getTheme()) !== null) { - array_unshift($paths, $theme->getViewPath($this->context)); - } - foreach ($paths as $path) { - $file = \Yii::getAlias($path . '/' . $view); - if ($file !== false && is_file($file)) { - return $file; + // e.g. "index" + if ($this->_owner instanceof Controller || $this->_owner instanceof Widget) { + $path = $this->_owner->getViewPath() . DIRECTORY_SEPARATOR . $view; + } elseif ($this->_owner !== null) { + $class = new \ReflectionClass($this->_owner); + $path = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; + } else { + $path = Yii::$application->getViewPath(); } - } - return $paths === array() ? $this->findAbsoluteViewFile($view) : false; - } - - /** - * Finds the view file corresponding to the given absolute view name. - * If the view name starts with double slashes `//`, the method will look for the view file - * under [[Application::getViewPath()]]. Otherwise, it will look for the view file under the - * view path of the currently active module. - * @param string $view the absolute view name - * @return string|boolean the view file path, or false if the view file cannot be found - */ - protected function findAbsoluteViewFile($view) - { - $app = \Yii::$application; - if (strncmp($view, '//', 2) !== 0 && $app->controller !== null) { - $module = $app->controller->module; + $file = $path . DIRECTORY_SEPARATOR . $view; + } elseif (strncmp($view, '//', 2) !== 0 && Yii::$application->controller !== null) { + // e.g. "/site/index" + $file = Yii::$application->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } else { - $module = $app; + // e.g. "//layouts/main" + $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } - if ($this->themeView && ($theme = $app->getTheme()) !== null) { - $paths[] = $theme->getViewPath($module); - } - $paths[] = $module->getViewPath(); - $view = ltrim($view, '/'); - foreach ($paths as $path) { - $file = \Yii::getAlias($path . '/' . $view); - if ($file !== false && is_file($file)) { - return $file; - } - } - return false; - } - /** - * Resolves the base paths that will be used to determine view files for relative view names. - * The method resolves the base path using the following algorithm: - * - * - If [[basePath]] is not empty, it is returned; - * - If [[context]] is a controller, it will return the subdirectory named as - * [[Controller::uniqueId]] under the controller's module view path; - * - If [[context]] is an object, it will return the `views` subdirectory under - * the directory containing the object class file. - * - Otherwise, it will return false. - * @return array the base paths - */ - protected function resolveBasePath() - { - if (!empty($this->basePath)) { - return (array)$this->basePath; - } elseif ($this->context instanceof Controller) { - return array($this->context->module->getViewPath() . '/' . $this->context->getUniqueId()); - } elseif ($this->context !== null) { - $class = new \ReflectionClass($this->context); - return array(dirname($class->getFileName()) . '/views'); + if (is_file($file)) { + if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { + $file = $theme->apply($file); + } + return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; } else { - return array(); + return false; } } /** - * Finds the layout file for the current [[context]]. - * The method will return false if [[context]] is not a controller. - * When [[context]] is a controller, the following algorithm is used to determine the layout file: + * Finds the layout file for the current [[owner]]. + * The method will return false if [[owner]] is not a controller. + * When [[owner]] is a controller, the following algorithm is used to determine the layout file: * - * - If `context.layout` is false, it will return false; - * - If `context.layout` is a string, it will look for the layout file under the [[Module::layoutPath|layout path]] + * - If `content` is not a controller or if `owner.layout` is false, it will return false; + * - If `owner.layout` is a string, it will look for the layout file under the [[Module::layoutPath|layout path]] * of the controller's parent module; - * - If `context.layout` is null, the following steps are taken to resolve the actual layout to be returned: + * - If `owner.layout` is null, the following steps are taken to resolve the actual layout to be returned: * * Check the `layout` property of the parent module. If it is null, check the grand parent module and so on * until a non-null layout is encountered. Let's call this module the *effective module*. * * If the layout is null or false, it will return false; @@ -415,15 +383,21 @@ class View extends Component * * The themed layout file will be returned if theme is enabled and the theme contains such a layout file. * - * @return string|boolean the layout file path, or false if the context does not need layout. + * @return string|boolean the layout file path, or false if the owner does not need layout. * @throws InvalidCallException if the layout file cannot be found */ public function findLayoutFile() { - if (!$this->context instanceof Controller || $this->context->layout === false) { + if ($this->layout === null || !$this->_owner instanceof Controller) { + $layout = Yii::$application->layout; + } elseif ($this->_owner->layout !== false) { + + } + if (!$this->_owner instanceof Controller || $this->_owner->layout === false) { return false; } - $module = $this->context->module; + /** @var $module Module */ + $module = $this->_owner->module; while ($module !== null && $module->layout === null) { $module = $module->module; } @@ -432,21 +406,35 @@ class View extends Component } $view = $module->layout; - if (($extension = FileHelper::getExtension($view)) === '') { + if (FileHelper::getExtension($view) === '') { $view .= '.php'; } if (strncmp($view, '@', 1) === 0) { - $file = \Yii::getAlias($view); + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) !== 0) { + // e.g. "main" + if ($this->_owner instanceof Controller || $this->_owner instanceof Widget) { + $path = $this->_owner->getViewPath() . DIRECTORY_SEPARATOR . $view; + } elseif ($this->_owner !== null) { + $class = new \ReflectionClass($this->_owner); + $path = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; + } else { + $path = Yii::$application->getViewPath(); + } + $file = $path . DIRECTORY_SEPARATOR . $view; + } elseif (strncmp($view, '//', 2) !== 0 && Yii::$application->controller !== null) { + // e.g. "/main" + $file = Yii::$application->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + // e.g. "//main" + $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } elseif (strncmp($view, '/', 1) === 0) { $file = $this->findAbsoluteViewFile($view); } else { - if ($this->themeView && ($theme = \Yii::$application->getTheme()) !== null) { - $paths[] = $theme->getLayoutPath($module); - } $paths[] = $module->getLayoutPath(); $file = false; foreach ($paths as $path) { - $f = \Yii::getAlias($path . '/' . $view); + $f = Yii::getAlias($path . '/' . $view); if ($f !== false && is_file($f)) { $file = $f; break; @@ -455,7 +443,7 @@ class View extends Component } if ($file === false || !is_file($file)) { throw new InvalidCallException("Unable to find the layout file for layout '{$module->layout}' (specified by " . get_class($module) . ")"); - } elseif ($this->localizeView) { + } elseif ($this->enableI18N) { return FileHelper::localize($file, $this->language, $this->sourceLanguage); } else { return $file; diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 686e58e..ba5eb82 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -102,4 +102,16 @@ class Widget extends Component { return new View($this); } + + /** + * Returns the directory containing the view files for this widget. + * The default implementation returns the 'views' subdirectory under the directory containing the widget class file. + * @return string the directory containing the view files for this widget. + */ + public function getViewPath() + { + $className = get_class($this); + $class = new \ReflectionClass($className); + return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; + } } \ No newline at end of file diff --git a/framework/util/FileHelper.php b/framework/util/FileHelper.php index d340338..c65e4f0 100644 --- a/framework/util/FileHelper.php +++ b/framework/util/FileHelper.php @@ -51,6 +51,20 @@ class FileHelper } /** + * Normalizes a file/directory path. + * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`, + * and any trailing directory separators will be removed. For example, '/home\demo/' on Linux + * will be normalized as '/home/demo'. + * @param string $path the file/directory path to be normalized + * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`. + * @return string the normalized file/directory path + */ + public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR) + { + return rtrim(strtr($path, array('/' => $ds, '\\' => $ds)), $ds); + } + + /** * Returns the localized version of a specified file. * * The searching is based on the specified language code. In particular,