<?php

/**
 * Class Sheep_Debug_Model_Controller captures information about the HTTP request, HTTP response and used controller.
 *
 * @category Sheep
 * @package  Sheep_Debug
 * @license  Copyright: Pirate Sheep, 2016
 * @link     https://piratesheep.com
 */
class Sheep_Debug_Model_Controller
{
    protected $serverParameters;

    protected $httpMethod;
    // request url path
    protected $requestOriginalPath;
    // request url path after rewrite (internal url path)
    protected $requestPath;
    protected $requestHeaders;
    protected $remoteIp;

    protected $routeName;
    protected $module;
    protected $class;
    protected $action;

    protected $sessionId;
    protected $cookies;
    protected $session;
    protected $getParameters;
    protected $postParameters;

    protected $responseCode;
    protected $responseHeaders;

    /**
     * Captures request, response and controller information.
     *
     * Sheep_Debug_Model_Controller constructor.
     * @param Mage_Core_Controller_Varien_Action $action
     */
    public function init($action=null)
    {
        $this->initFromAction($action);
        $this->initFromGlobals();
    }

    /**
     * Captures request, response and controller information.
     *
     * Sheep_Debug_Model_Controller constructor.
     * @param Mage_Core_Controller_Varien_Action $action
     */
    public function initFromAction($action)
    {
        if (!$action) {
            return;
        }

        /** @var Mage_Core_Controller_Request_Http $request */
        $request = $action->getRequest();

        $this->httpMethod = $request->getMethod();
        $this->requestOriginalPath = $request->getOriginalPathInfo();
        $this->requestPath = $request->getPathInfo();
        $this->remoteIp = Mage::helper('core/http')->getRemoteAddr();

        $this->routeName = $request->getRouteName();
        $this->module = $request->getControllerModule();
        $this->class = get_class($action);
        $this->action = $action->getActionMethodName($request->getActionName());

    }

    /**
     * Initialize generic request attributes stored in PHP global variables (POST, GET, etc)
     */
    public function initFromGlobals()
    {
        /** @var Sheep_Debug_Helper_Http $helper */
        $helper = Mage::helper('sheep_debug/http');

        $this->httpMethod = $helper->getHttpMethod();
        $this->requestPath = $this->requestOriginalPath = $helper->getRequestPath();
        $this->remoteIp = $helper->getRemoteAddr();

        $this->serverParameters = $helper->getGlobalServer();
        $this->requestHeaders = $helper->getAllHeaders();
        $this->cookies = $helper->getGlobalCookie();
        $this->session = $helper->getGlobalSession();
        $this->getParameters = $helper->getGlobalGet();
        $this->postParameters = $helper->getGlobalPost();
    }

    /**
     * Initialise Magento session related attributes
     */
    public function initFromSession()
    {
        $this->sessionId = Mage::getSingleton('core/session')->getEncryptedSessionId();
    }

    /**
     * Initialize response properties
     *
     * @param Mage_Core_Controller_Response_Http $httpResponse
     */
    public function addResponseInfo(Mage_Core_Controller_Response_Http $httpResponse)
    {
        $this->responseCode = $httpResponse->getHttpResponseCode();

        $this->responseHeaders = array();
        $headers = $httpResponse->getHeaders();
        foreach ($headers as $header) {
            $this->responseHeaders[$header['name']] = $header['value'];
        }
    }

    /**
     * Returns request path
     *
     * @return string
     */
    public function getRequestPath()
    {
        return $this->requestPath;
    }

    /**
     * Returns route name
     *
     * @return mixed
     */
    public function getRouteName()
    {
        return $this->routeName;
    }

    /**
     * Returns module name
     *
     * @return string
     */
    public function getModule()
    {
        return $this->module;
    }

    /**
     * Returns controller class that handled the request
     *
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     * Returns controller's method that handled the request
     *
     * @return string
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * Returns a meaningful code reference that includes controller class and action method
     *
     * @return string
     */
    public function getReference()
    {
        return $this->getClass() . '::' . $this->getAction();
    }

    /**
     * Returns cookies state during the request
     *
     * @return mixed
     */
    public function getCookies()
    {
        return $this->cookies;
    }

    /**
     * Returns session state during the request
     *
     * @return mixed
     */
    public function getSession()
    {
        return $this->session;
    }

    /**
     * Request request's get parameters
     *
     * @return mixed
     */
    public function getGetParameters()
    {
        return $this->getParameters;
    }

    /**
     * Returns request's post parameters
     *
     * @return mixed
     */
    public function getPostParameters()
    {
        return $this->postParameters;
    }

    /**
     * Returns request handling attributes
     *
     * @return array
     */
    public function getRequestAttributes()
    {
        return array(
            'route'  => $this->routeName,
            'module' => $this->module,
            'action' => $this->getReference()
        );
    }

    /**
     * Returns response code
     *
     * @return mixed
     */
    public function getResponseCode()
    {
        return $this->responseCode;
    }

    /**
     * Returns session id during the request
     *
     * @return string
     */
    public function getSessionId()
    {
        return $this->sessionId;
    }

    /**
     * Returns request's http method
     *
     * @return mixed
     */
    public function getHttpMethod()
    {
        return $this->httpMethod;
    }

    /**
     * Returns request's original path (before Magento's rewrite)
     *
     * @return string
     */
    public function getRequestOriginalPath()
    {
        return $this->requestOriginalPath;
    }

    /**
     * Returns request's headers
     *
     * @return array
     */
    public function getRequestHeaders()
    {
        return $this->requestHeaders;
    }

    /**
     * Returns server parameters during the request
     *
     * @return mixed
     */
    public function getServerParameters()
    {
        return $this->serverParameters;
    }

    /**
     * Returns response headers
     *
     * @return mixed
     */
    public function getResponseHeaders()
    {
        return $this->responseHeaders;
    }

    /**
     * Returns remote ip
     *
     * @return mixed
     */
    public function getRemoteIp()
    {
        return $this->remoteIp;
    }

}


<?php

/**
 * Class Sheep_Debug_ConfigController
 *
 * @category Sheep
 * @package  Sheep_Debug
 * @license  Copyright: Pirate Sheep, 2016
 * @link     https://piratesheep.com
 */
class Sheep_Debug_ConfigController extends Sheep_Debug_Controller_Front_Action
{
    /**
     * PHP info Action
     */
    public function phpinfoAction()
    {
        phpinfo();
    }

    /**
     * Download configuration as text or xml action
     */
    public function downloadAction()
    {
        $type = $this->getRequest()->getParam('type', 'xml');
        /** @var Mage_Core_Model_Config_Element $configNode */
        $configNode = Mage::app()->getConfig()->getNode();

        switch ($type) {
            case 'txt';
                $this->downloadAsText($configNode);
                break;
            case 'xml':
            default:
                $this->downloadAsXml($configNode);
        }
    }

    /**
     * Force enable Varien Profiler action
     */
    public function enableVarienProfilerAction()
    {
        $this->getService()->setVarienProfilerStatus(1);
        $this->getService()->flushCache();

        $this->_redirectReferer();
    }

    /**
     * Disable forced activation of Varien Profiler
     */
    public function disableVarienProfilerAction()
    {
        $this->getService()->setVarienProfilerStatus(0);
        $this->getService()->flushCache();

        $this->_redirectReferer();
    }

    /**
     * Prepares response with configuration as text
     *
     * @param Mage_Core_Model_Config_Element $configNode
     */
    public function downloadAsText(Mage_Core_Model_Config_Element $configNode)
    {
        $items = array();
        Mage::helper('sheep_debug')->xml2array($configNode, $items);

        $content = '';
        foreach ($items as $key => $value) {
            $content .= "$key = $value\n";
        }

        $this->_prepareDownloadResponse('config.txt', $content, 'text/plain');
    }

    /**
     * Prepares response with configuration as xml
     *
     * @param Mage_Core_Model_Config_Element $configNode
     */
    public function downloadAsXml(Mage_Core_Model_Config_Element $configNode)
    {
        $this->_prepareDownloadResponse('config.xml', $configNode->asXML(), 'text/xml');
    }

}


<?php

/**
 * Class Sheep_Debug_Model_Core_Email_Template rewrites core/email_template class in order
 * to capture e-mail variables.
 *
 * @method string getSenderName()
 * @method string getSenderEmail()
 *
 * @category Sheep
 * @package  Sheep_Debug
 * @license  Copyright: Pirate Sheep, 2016
 * @link     https://piratesheep.com
 */
trait Sheep_Debug_Model_Core_Email_Template_Capture
{

    /**
     * Calls real send() method
     *
     * @param array|string $email
     * @param null         $name
     * @param array        $variables
     * @return bool
     */
    public function parentSend($email, $name = null, array $variables = array())
    {
        return parent::send($email, $name, $variables);
    }

    /**
     * Overwrites parent method to capture e-mail details
     *
     * @param array|string $email
     * @param null         $name
     * @param array        $variables
     * @return bool
     */
    public function send($email, $name = null, array $variables = array())
    {
        // store a reference to mail object that get populate by parent send()
        $zendMail = $this->getMail();

        $result = $this->parentSend($email, $name, $variables);

        try {
            $this->addEmailToProfile($email, $name, $variables, $result, $zendMail);
        } catch (Exception $e) {
            Mage::logException($e);
        }

        return $result;
    }

    /**
     * Adds e-mail information on request profiler
     *
     * @param           $email
     * @param           $name
     * @param           $variables
     * @param           $result
     * @param Zend_Mail $mail
     */
    public function addEmailToProfile($email, $name, $variables, $result, Zend_Mail $mail)
    {
        $emailCapture = Mage::getModel('sheep_debug/email');

        $subject = $this->decodeSubject($mail->getSubject());
        $body = $this->getContent($mail);

        $emailCapture->setFromName($this->getSenderName());
        $emailCapture->setFromEmail($this->getSenderEmail());

        $emailCapture->setToEmail($email);
        $emailCapture->setToName($name);
        $emailCapture->setSubject($subject);
        $emailCapture->setIsPlain($this->isPlain());
        $emailCapture->setBody($body);
        $emailCapture->setIsAccepted($result);
        $emailCapture->setVariables($variables);
        $emailCapture->setIsSmtpDisabled((bool)Mage::getStoreConfigFlag('system/smtp/disable'));

        Mage::getSingleton('sheep_debug/observer')->getRequestInfo()->addEmail($emailCapture);
    }

    /**
     * Returns raw content attached to specified mail object
     *
     * @param Zend_Mail $mail
     * @return string
     */
    public function getContent(Zend_Mail $mail)
    {
        $hasQueue = $this->hasQueue();

        if ($hasQueue && $queue = $this->getQueue()) {
            return $queue->getMessageBody();
        }

        /** @var Zend_Mime_Part $mimePart */
        $mimePart = $this->isPlain() ? $mail->getBodyText() : $mail->getBodyHtml();

        return $mimePart ? $this->getPartDecodedContent($mimePart) : '';
    }

    /**
     * Returns raw content of e-mail message. Abstract Zend_Mime_Part interface changes between 1.11.0 and 1.12.0
     *
     * @param Zend_Mime_Part $mimePart
     * @return String
     */
    public function getPartDecodedContent(Zend_Mime_Part $mimePart)
    {
        // getRawContent is not available in Zend 1.11 (Magento CE 1.7)
        if (method_exists($mimePart, 'getRawContent')) {
            return $mimePart->getRawContent();
        }

        $content = '';
        if (method_exists($mimePart, 'getContent')) {
            $encoding = $mimePart->encoding;
            $mimePart->encoding = 'none';
            $content = $mimePart->getContent();
            $mimePart->encoding = $encoding;
        }

        return $content;
    }

    /**
     * Returns raw subject
     *
     * @param $subject
     * @return string
     */
    public function decodeSubject($subject)
    {
        if ($this->hasQueue() && $queue = $this->getQueue()) {
            return $queue->getMessageParameters('subject');
        }

        return base64_decode(substr($subject, strlen('=?utf-8?B?'), -1 * strlen('?=')));
    }

}

if (Mage::helper('core')->isModuleEnabled('Aschroder_SMTPPro')) {

    class Sheep_Debug_Model_Core_Email_Template extends Aschroder_SMTPPro_Model_Email_Template
    {
        use Sheep_Debug_Model_Core_Email_Template_Capture;
    }

} else if(Mage::helper('core')->isModuleEnabled('Ebizmarts_Mandrill')) {

    class Sheep_Debug_Model_Core_Email_Template extends Ebizmarts_Mandrill_Model_Email_Template
    {
        use Sheep_Debug_Model_Core_Email_Template_Capture;
    }

} else {

    class Sheep_Debug_Model_Core_Email_Template extends Mage_Core_Model_Email_Template
    {
        use Sheep_Debug_Model_Core_Email_Template_Capture;
    }
}


<?php

/**
 * Class Sheep_Debug_Helper_Data
 *
 * @category Sheep
 * @package  Sheep_Debug
 * @license  Copyright: Pirate Sheep, 2016
 * @link     https://piratesheep.com
 */
class Sheep_Debug_Helper_Data extends Mage_Core_Helper_Data
{
    const DEBUG_OPTIONS_ENABLE_PATH = 'sheep_debug/options/enable';
    const DEBUG_OPTION_PERSIST_PATH = 'sheep_debug/options/persist';
    const DEBUG_OPTION_PERSIST_EXPIRATION_PATH = 'sheep_debug/options/persist_expiration';
    const DEBUG_OPTION_FORCE_VARIEN_PROFILE_PATH = 'sheep_debug/options/force_varien_profile';
    const DEBUG_OPTION_USE_STORE_LOCALE = 'sheep_debug/options/use_store_locale';
    const DEBUG_OPTION_CAPTURE_SQL_STACKTRACE = 'sheep_debug/options/capture_sql_stacktrace';
    const DEBUG_OPTION_STRIP_ZEND_DB_TRACES = 'sheep_debug/options/strip_zend_db_traces';
    const DEBUG_OPTION_TRIM_MAGENTO_DIR = 'sheep_debug/options/trim_magento_basedir';

    /**
     * Checks if module is enabled based on configuration
     *
     * @return bool
     */
    public function isEnabled()
    {
        if(!Mage::isInstalled()) {
            return false;
        }

        return (bool)Mage::getStoreConfig(self::DEBUG_OPTIONS_ENABLE_PATH);
    }

    /**
     * Returns module name (e.g Sheep_Debug)
     *
     * @return string
     */
    public function getModuleName()
    {
        return $this->_getModuleName();
    }

    /**
     * Returns module version number
     *
     * @return string
     */
    public function getModuleVersion()
    {
        /** @var Mage_Core_Model_Config_Element $moduleConfig */
        $moduleConfig = $this->getConfig()->getModuleConfig($this->getModuleName());
        return (string)$moduleConfig->version;
    }

    /**
     * Returns module's directory
     *
     * @return string
     */
    public function getModuleDirectory()
    {
        return $this->getConfig()->getModuleDir('', $this->getModuleName());
    }

    /**
     * Checks if Magento Developer Mode is enabled
     * @return bool
     */
    public function getIsDeveloperMode()
    {
        return Mage::getIsDeveloperMode();
    }

    /**
     * Returns filename for general logging
     *
     * @param $store
     * @return string
     */
    public function getLogFilename($store)
    {
        return (string)Mage::getStoreConfig('dev/log/file', $store);
    }

    /**
     * Returns filename for exception logging
     *
     * @param $store
     * @return string
     */
    public function getExceptionLogFilename($store)
    {
        return (string)Mage::getStoreConfig('dev/log/exception_file', $store);
    }

    /**
     * Returns results as assoc array for specified SQL query
     *
     * @param string $query
     * @param array  $queryParams
     * @return array
     * @throws Zend_Db_Statement_Exception
     */
    public function runSql($query, $queryParams = array())
    {
        /** @var Magento_Db_Adapter_Pdo_Mysql $connection */
        $connection = Mage::getSingleton('core/resource')->getConnection('core_write');
        /** @var Varien_Db_Statement_Pdo_Mysql $statement */
        $statement = $connection->query($query, $queryParams);

        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Returns SQL profiler
     *
     * @return Zend_Db_Profiler
     */
    public function getSqlProfiler()
    {
        /** @var Magento_Db_Adapter_Pdo_Mysql $connection */
        $connection = Mage::getSingleton('core/resource')->getConnection('core_write');

        return $connection->getProfiler();
    }

    /**
     * Flattens an xml tree into an associate array where key represents path
     *
     * @param Mage_Core_Model_Config_Element $xml
     * @param array                          $arr
     * @param string                         $parentKey
     * @return array|void
     */
    public function xml2array($xml, array &$arr, $parentKey = '')
    {
        if (!$xml) {
            return;
        }

        if (count($xml->children()) == 0) {
            $arr[$parentKey] = (string)$xml;
        } else {
            foreach ($xml->children() as $key => $item) {
                $key = $parentKey ? $parentKey . DS . $key : $key;
                $this->xml2array($item, $arr, $key);
            }
        }

        return $arr;
    }

    /**
     * Decides if we need to capture request information.
     *
     * We don't capture requests if:
     *      - requests belons to our module
     *      - we're not allowed to show toolbar (module disabled, dev mod is off)
     */
    public function canCapture()
    {
        return !$this->isSheepDebugRequest($this->_getRequest()) && $this->canShowToolbar();
    }

    /**
     * Checks if current request belongs to our module by verifying if its request path starts with our route name.
     *
     * We cannot verify controller module becuase request is not yet dispatched.
     *
     * @param Mage_Core_Controller_Request_Http $request
     * @return bool
     */
    public function isSheepDebugRequest(Mage_Core_Controller_Request_Http $request)
    {
        return strpos($request->getPathInfo(), '/sheep_debug/') === 0;
    }

    /**
     * Decides if we need to render toolbar
     *
     * Rules:
     *      - never show toolbar if it is disabled from Admin
     *      - show toolbar if it's enabled and if developer mode is active
     *      - show toolbar if it's enabled and if current ip is in the allowed list of ips
     *
     * @return bool
     */
    public function canShowToolbar()
    {
        if (!$this->isEnabled()) {
            return false;
        }

        // ignore IP white listing if developer mode is on
        if (Mage::getIsDeveloperMode()) {
            return true;
        }

        // IP is the allowed list
        return $this->isDevAllowed();
    }

    /**
     * Checks if current customer is allowed to access our controllers
     *
     * @return bool
     */
    public function isAllowed()
    {
        return $this->canShowToolbar();
    }

    /**
     * Returns specified number formatted based on current locale
     *
     * @param     $number
     * @param int $precision
     * @return string
     * @throws Zend_Locale_Exception
     */
    public function formatNumber($number, $precision = 2)
    {
        $locale = Mage::app()->getLocale()->getLocale();
        return Zend_Locale_Format::toNumber($number, array('locale' => $locale, 'precision' => $precision));
    }

    /**
     * Returns readable file size
     *
     * @param int $size size in bytes
     * @param int $precision
     * @return string
     */
    public function formatMemorySize($size, $precision = 2)
    {
        $sizes = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
        if ($size == 0) {
            return $this->__('n/a');
        } else {
            $value = round($size / pow(1000, ($i = floor(log($size, 1000)))), $precision);
            $unitIndex = (int)$i;
            return $this->__('%s%s', $this->formatNumber($value, $precision), $sizes[$unitIndex]);
        }
    }

    /**
     * Formats a stack trace array generated by debug_backtrace
     *
     * Inspired by @see mageDebugBacktrace
     *
     * @param array  $trace
     * @param string $stripFilepath
     * @return string
     */
    public function formatStacktrace(array $trace, $stripFilepath = '', $trimPath = '')
    {
        $out = '';
        foreach ($trace as $index => $row) {

            if ($stripFilepath && isset($row['file']) && strpos($row['file'], $stripFilepath) !== false) {
                continue;
            }

            if ($trimPath && isset($row['file'])) {
                $row['file'] = str_replace($trimPath, '', $row['file']);
            }

            if (isset($row['file'])) {
                $out .= "[$index] {$row['file']}:{$row['line']}\n";
            } else {
                // sometimes there is undefined index 'file'
                $out .= "[$index] (?) {$row['class']}:{$row['function']}\n";
            }

        }

        return $out;
    }

    /**
     * Returns peak memory in bytes
     *
     * @return int
     */
    public function getMemoryUsage()
    {
        return memory_get_peak_usage(TRUE);
    }

    /**
     * Returns current script duration in seconds.
     * NULL if we cannot determine.
     *
     * @return float|NULL
     */
    public function getCurrentScriptDuration()
    {
        return function_exists('xdebug_time_index') ? xdebug_time_index() : null;
    }

    /**
     * Sort callback for objects that have getCount()
     * @see Sheep_Debug_Model_Model
     *
     * @param Sheep_Debug_Model_Model|Sheep_Debug_Model_Collection $a
     * @param Sheep_Debug_Model_Model|Sheep_Debug_Model_Collection $b
     * @return int
     */
    public static function sortModelCmp($a, $b)
    {
        if ($a->getCount() == $b->getCount())
            return 0;
        return ($a->getCount() < $b->getCount()) ? 1 : -1;
    }

    /**
     * Sorts specified array based on getCount()
     *
     * @param array $objects
     */
    public function sortByCount(&$objects)
    {
        usort($objects, array('Sheep_Debug_Helper_Data', 'sortModelCmp'));
    }

    /**
     * Returns filepath for specified block class
     *
     * @param $blockClass
     * @return bool|string
     */
    public function getBlockFilename($blockClass)
    {
        return mageFindClassFile($blockClass);
    }

    /**
     * Returns all xml files that contains layout updates.
     *
     * @param int    $storeId store identifier
     * @param string $designArea
     * @return array
     */
    public function getLayoutUpdatesFiles($storeId, $designArea)
    {
        $updatesRoot = $this->getConfig()->getNode($designArea . '/layout/updates');

        // Find files with layout updates
        $updateFiles = array();

        /** @var Mage_Core_Model_Config_Element $updateNode */
        foreach ($updatesRoot->children() as $updateNode) {
            if ($updateNode->file) {
                $module = $updateNode->getAttribute('module');
                if ($module && Mage::getStoreConfigFlag('advanced/modules_disable_output/' . $module, $storeId)) {
                    continue;
                }
                $updateFiles[] = (string)$updateNode->file;
            }
        }
        // custom local layout updates file
        $updateFiles[] = 'local.xml';

        return $updateFiles;
    }

    /**
     * Checks if this is Magento Enterprise
     *
     * @return bool
     */
    public function isMagentoEE()
    {
        return method_exists('Mage', 'getEdition') && Mage::getEdition() == Mage::EDITION_ENTERPRISE;
    }

    /**
     * Checks configuration if we can save request profile information
     *
     * @return bool
     */
    public function canPersist()
    {
        return (bool)Mage::getStoreConfig(self::DEBUG_OPTION_PERSIST_PATH);
    }

    /**
     * Returns if disable persistence cookie is set.
     *
     * @return bool
     */
    public function hasDisablePersistenceCookie()
    {
        /** @var Mage_Core_Model_Cookie $cookie */
        $cookie = Mage::getSingleton('core/cookie');
        return (boolean) $cookie->get('sheep_debug_disable_persist');
    }

    /**
     * Lifetime of persisted requests
     *
     * @return int
     */
    public function getPersistLifetime()
    {
        return (int)Mage::getStoreConfig(self::DEBUG_OPTION_PERSIST_EXPIRATION_PATH);
    }

    /**
     * Checks if configuration allows to enable Varien Profiler
     *
     * @return bool
     */
    public function canEnableVarienProfiler()
    {
        return (bool)Mage::getStoreConfig(self::DEBUG_OPTION_FORCE_VARIEN_PROFILE_PATH);
    }

    /**
     * Checks if we want to capture stack trace for SQL
     *
     * @return bool
     */
    public function canEnableSqlStacktrace()
    {
        return (bool)Mage::getStoreConfig(self::DEBUG_OPTION_CAPTURE_SQL_STACKTRACE);
    }

    /**
     * Checks if we should not display traces related to Zend Db files
     *
     * @return bool
     */
    public function canStripZendDbTrace()
    {
        return (bool)Mage::getStoreConfig(self::DEBUG_OPTION_STRIP_ZEND_DB_TRACES);
    }

    /**
     * Checks if we can remove Magento base dir from stack trace file paths
     *
     * @return bool
     */
    public function canTrimMagentoBaseDir()
    {
        return (bool)Mage::getStoreConfig(self::DEBUG_OPTION_TRIM_MAGENTO_DIR);
    }

    /**
     * Checks if we need to translate or format extension content based on
     * store locale
     *
     * @return bool
     */
    public function useStoreLocale()
    {
        return (bool)Mage::getStoreConfig(self::DEBUG_OPTION_USE_STORE_LOCALE);
    }

    /**
     * Returns Magento configuration instance
     *
     * @return Mage_Core_Model_Config
     */
    protected function getConfig()
    {
        return Mage::getConfig();
    }

    /**
     * Formats the blockname used in the observer::onBlockToHtml method
     *
     * @param Mage_Core_Block_Abstract $block
     * @return string
     */
    public function getBlockName(Mage_Core_Block_Abstract $block)
    {
        $blockName = $block->getParentBlock() ?
            "{$block->getParentBlock()->getNameInLayout()}_{$block->getNameInLayout()}" :
            "{$block->getNameInLayout()}";

        if ($block->getBlockAlias()) {
            $blockName .= "_{$block->getBlockAlias()}";
        }

        return $blockName;
    }
}


<?php  namespace Jackiedo\DotenvEditor\Console\Commands;

use Illuminate\Console\Command;
use Jackiedo\DotenvEditor\Console\Traits\CreateCommandInstanceTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class DotenvGetKeysCommand extends Command
{
    use CreateCommandInstanceTrait;

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'dotenv:get-keys';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'List all setter in the .env file';

    /**
     * The .env file path
     *
     * @var string|null
     */
    protected $filePath = null;

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function fire()
    {
        $filePath       = $this->stringToType($this->option('filepath'));
        $this->filePath = (is_string($filePath)) ? base_path($filePath) : null;

        $allKeys = $this->editor->load($this->filePath)->getKeys();
        $output = [];
        foreach ($allKeys as $key => $info) {
            $data = [
                'key'     => $key,
                'export'  => ($info['export']) ? 'true' : 'false',
                'value'   => $info['value'],
                'comment' => $info['comment'],
                'line'    => $info['line']
            ];
            $output[] = $data;
        }

        $total   = count($output);

        $headers = ['Key', 'Use export', 'Value', 'Comment', 'In line'];

        $this->line('Loading keys in your file...');
        $this->line('');
        $this->table($headers, $output);
        $this->line('');
        $this->info("You have total {$total} keys in your file");
    }

    /**
     * Convert string to corresponding type
     *
     * @param  string $string
     *
     * @return mixed
     */
    protected function stringToType($string)
    {
        if (is_string($string)) {
            switch (true) {
                case ($string == 'null' || $string == 'NULL'):
                    $string = null;
                    break;

                case ($string == 'true' || $string == 'TRUE'):
                    $string = true;
                    break;

                case ($string == 'false' || $string == 'FALSE'):
                    $string = false;
                    break;

                default:
                    break;
            }
        }

        return $string;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            array('filepath', null, InputOption::VALUE_OPTIONAL, 'The file path should use to load for working. Do not use if you want to load file .env at root application folder.')
        ];
    }
}


<?php  namespace Jackiedo\DotenvEditor\Console\Commands;

use Illuminate\Console\Command;
use Jackiedo\DotenvEditor\Console\Traits\CreateCommandInstanceTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class DotenvBackupCommand extends Command
{
    use CreateCommandInstanceTrait;

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'dotenv:backup';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Backup the .env file';

    /**
     * The .env file path
     *
     * @var string|null
     */
    protected $filePath = null;

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function fire()
    {
        $filePath       = $this->stringToType($this->option('filepath'));
        $this->filePath = (is_string($filePath)) ? base_path($filePath) : null;

        $this->line('Backing up your file...');

        $backup = $this->editor->load($this->filePath)->backup()->getLatestBackup();

        $this->info("Your file was backed up successfully at path [{$backup['filepath']}].");
    }

    /**
     * Convert string to corresponding type
     *
     * @param  string $string
     *
     * @return mixed
     */
    protected function stringToType($string)
    {
        if (is_string($string)) {
            switch (true) {
                case ($string == 'null' || $string == 'NULL'):
                    $string = null;
                    break;

                case ($string == 'true' || $string == 'TRUE'):
                    $string = true;
                    break;

                case ($string == 'false' || $string == 'FALSE'):
                    $string = false;
                    break;

                default:
                    break;
            }
        }

        return $string;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            array('filepath', null, InputOption::VALUE_OPTIONAL, 'The file path will be backed up. Do not use if you want to backup file .env at root application folder.')
        ];
    }
}


<?php  namespace Jackiedo\DotenvEditor\Console\Commands;

use Illuminate\Console\Command;
use Jackiedo\DotenvEditor\Console\Traits\CreateCommandInstanceTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class DotenvGetBackupsCommand extends Command
{
    use CreateCommandInstanceTrait;

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'dotenv:get-backups';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'List all the .env file backup versions';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function fire()
    {
        $headers = ['File name', 'File path', 'Created at'];
        $backups = ($this->option('latest')) ? [$this->editor->getLatestBackup()] : $this->editor->getBackups();

        if ($this->option('latest')) {
            $latest = $this->editor->getLatestBackup();
            if (! is_null($latest)) {
                $backups = [$latest];
                $total = 1;
            } else {
                $total = 0;
            }
        } else {
            $backups = $this->editor->getBackups();
            $total   = count($backups);
        }

        $this->line('Loading backup files...');
        $this->line('');

        if ($total == 0) {
            $this->info("You have not any backup file");
        } elseif ($total == 1) {
            $this->table($headers, $backups);
            $this->line('');
            $this->info("There is 1 backup file found from your request");
        } else {
            $this->table($headers, $backups);
            $this->line('');
            $this->info("There are {$total} backup files found from your request");
        }
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            array('latest', 'l', InputOption::VALUE_NONE, 'Only get latest version from backup files.')
        ];
    }
}


<?php  namespace Jackiedo\DotenvEditor\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Jackiedo\DotenvEditor\Console\Traits\CreateCommandInstanceTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class DotenvSetKeyCommand extends Command
{
    use ConfirmableTrait, CreateCommandInstanceTrait;

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'dotenv:set-key';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Add new or update one setter into the .env file';

    /**
     * The .env file path
     *
     * @var string|null
     */
    protected $filePath = null;

    /**
     * Determine restoring the .env file if not exists
     *
     * @var boolean
     */
    protected $forceRestore = false;

    /**
     * The file path should use to restore
     *
     * @var string|null
     */
    protected $retorePath = null;

    /**
     * The key name use to add or update
     *
     * @var string
     */
    protected $key = 'NEW_ENV_KEY';

    /**
     * Value of key
     *
     * @var mixed
     */
    protected $value = null;

    /**
     * Comment for key
     *
     * @var mixed
     */
    protected $comment = null;

    /**
     * Determine leading the key with 'export '
     *
     * @var boolean
     */
    protected $exportKey = false;

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function fire()
    {
        $this->transferInputsToProperties();

        if (! $this->confirmToProceed()) {
            return false;
        }

        $this->line('Setting key in your file...');

        $this->editor->load($this->filePath, $this->forceRestore, $this->restorePath);
        $this->editor->setKey($this->key, $this->value, $this->comment, $this->exportKey);
        $this->editor->save();

        $this->info("The key [{$this->key}] is setted successfully with value [{$this->value}].");
    }

    /**
     * Transfer inputs to properties of editing
     *
     * @return void
     */
    protected function transferInputsToProperties()
    {
        $filePath       = $this->stringToType($this->option('filepath'));
        $this->filePath = (is_string($filePath)) ? base_path($filePath) : null;

        $this->forceRestore = $this->option('restore');

        $restorePath       = $this->stringToType($this->option('restore-path'));
        $this->restorePath = (is_string($restorePath)) ? base_path($restorePath) : null;

        $this->key       = $this->argument('key');
        $this->value     = $this->stringToType($this->argument('value'));
        $this->comment   = $this->stringToType($this->argument('comment'));
        $this->exportKey = $this->option('export-key');
    }

    /**
     * Convert string to corresponding type
     *
     * @param  string $string
     *
     * @return mixed
     */
    protected function stringToType($string)
    {
        if (is_string($string)) {
            switch (true) {
                case ($string == 'null' || $string == 'NULL'):
                    $string = null;
                    break;

                case ($string == 'true' || $string == 'TRUE'):
                    $string = true;
                    break;

                case ($string == 'false' || $string == 'FALSE'):
                    $string = false;
                    break;

                default:
                    break;
            }
        }

        return $string;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [
            array('key', InputArgument::REQUIRED, 'Key name will be added or updated.'),
            array('value', InputArgument::OPTIONAL, 'Value want to set for this key.'),
            array('comment', InputArgument::OPTIONAL, 'Comment want to set for this key. Type "false" to clear comment for exists key.')
        ];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            array('filepath', null, InputOption::VALUE_OPTIONAL, 'The file path should use to load for working. Do not use if you want to load file .env at root application folder.'),
            array('restore', 'r', InputOption::VALUE_NONE, 'Restore the loaded file from backup or special file if the loaded file is not found.'),
            array('restore-path', null, InputOption::VALUE_OPTIONAL, 'The special file path should use to restore from. Do not use if you want to restore from latest backup file.'),
            array('export-key', 'e', InputOption::VALUE_NONE, 'Leading before key name with "export " command.'),
            array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.')
        ];
    }
}


<?php  namespace Jackiedo\DotenvEditor\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Jackiedo\DotenvEditor\Console\Traits\CreateCommandInstanceTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class DotenvRestoreCommand extends Command
{
    use ConfirmableTrait, CreateCommandInstanceTrait;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $name = 'dotenv:restore';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Restore the .env file from backup or special file';

    /**
     * The .env file path
     *
     * @var string|null
     */
    protected $filePath = null;

    /**
     * The file path should use to restore
     *
     * @var string|null
     */
    protected $retorePath = null;

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function fire()
    {
        $this->transferInputsToProperties();

        if (! $this->confirmToProceed()) {
            return false;
        }

        $this->line('Restoring your file...');

        $this->editor->load($this->filePath)->restore($this->restorePath);

        $this->info("Your file is restored successfully");
    }

    /**
     * Transfer inputs to properties of editing
     *
     * @return void
     */
    protected function transferInputsToProperties()
    {
        $filePath       = $this->stringToType($this->option('filepath'));
        $this->filePath = (is_string($filePath)) ? base_path($filePath) : null;

        $restorePath       = $this->stringToType($this->option('restore-path'));
        $this->restorePath = (is_string($restorePath)) ? base_path($restorePath) : null;
    }

    /**
     * Convert string to corresponding type
     *
     * @param  string $string
     *
     * @return mixed
     */
    protected function stringToType($string)
    {
        if (is_string($string)) {
            switch (true) {
                case ($string == 'null' || $string == 'NULL'):
                    $string = null;
                    break;

                case ($string == 'true' || $string == 'TRUE'):
                    $string = true;
                    break;

                case ($string == 'false' || $string == 'FALSE'):
                    $string = false;
                    break;

                default:
                    break;
            }
        }

        return $string;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            array('filepath', null, InputOption::VALUE_OPTIONAL, 'The .env file path will be restored. Do not use if you want to restore file .env at root application folder.'),
            array('restore-path', null, InputOption::VALUE_OPTIONAL, 'The special file path should use to restore. Do not use if you want to restore from latest backup file.'),
            array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.')
        ];
    }
}


<?php namespace Jackiedo\DotenvEditor;

use Jackiedo\DotenvEditor\Contracts\DotenvFormatter as DotenvFormatterContract;
use Jackiedo\DotenvEditor\Contracts\DotenvWriter as DotenvWriterContract;
use Jackiedo\DotenvEditor\Exceptions\UnableWriteToFileException;

/**
 * The DotenvWriter writer.
 *
 * @package Jackiedo\DotenvEditor
 * @author Jackie Do <anhvudo@gmail.com>
 */
class DotenvWriter implements DotenvWriterContract
{
    /**
     * The content buffer
     *
     * @var string
     */
    protected $buffer;

    /**
     * The formatter instance
     *
     * @var \Jackiedo\DotenvEditor\DotenvFormatter
     */
    protected $formatter;

    /**
     * Create a new writer instance
     *
     * @param Jackiedo\DotenvEditor\Contracts\DotenvFormatter   $formatter
     */
    public function __construct(DotenvFormatterContract $formatter)
    {
        $this->formatter = $formatter;
    }

    /**
     * Tests file for writability. If the file doesn't exist, check
     * the parent directory for writability so the file can be created.
     *
     * @throws \Jackiedo\DotenvEditor\Exceptions\UnableWriteToFileException
     *
     * @return void
     */
    protected function ensureFileIsWritable($filePath)
    {
        if ((is_file($filePath) && !is_writable($filePath)) || (!is_file($filePath) && !is_writable(dirname($filePath)))) {
            throw new UnableWriteToFileException(sprintf('Unable to write to the file at %s.', $filePath));
        }
    }

    /**
     * Set buffer with content
     *
     * @param  string $content
     *
     * @return DotenvWriter
     */
    public function setBuffer($content)
    {
        $this->buffer = $content;
        return $this;
    }

    /**
     * Return content in buffer
     *
     * @return string
     */
    public function getBuffer()
    {
        return $this->buffer;
    }

    /**
     * Append new line to buffer
     *
     * @param  string|null  $text
     *
     * @return DotenvWriter
     */
    protected function appendLine($text = null)
    {
        $this->buffer .= $text . PHP_EOL;
        return $this;
    }

    /**
     * Append empty line to buffer
     *
     * @return DotenvWriter
     */
    public function appendEmptyLine()
    {
        return $this->appendLine();
    }

    /**
     * Append comment line to buffer
     *
     * @param  string $comment
     *
     * @return DotenvWriter
     */
    public function appendCommentLine($comment)
    {
        return $this->appendLine('# '.$comment);
    }

    /**
     * Append one setter to buffer
     *
     * @param  string       $key
     * @param  string|null  $value
     * @param  string|null  $comment
     * @param  boolean      $export
     *
     * @return DotenvWriter
     */
    public function appendSetter($key, $value = null, $comment = null, $export = false)
    {
        $line = $this->formatter->formatSetterLine($key, $value, $comment, $export);

        return $this->appendLine($line);
    }

    /**
     * Update one setter in buffer
     *
     * @param  string       $key
     * @param  string|null  $value
     * @param  string|null  $comment
     * @param  boolean      $export
     *
     * @return DotenvWriter
     */
    public function updateSetter($key, $value = null, $comment = null, $export = false)
    {
        $pattern = "/^(export\h)?\h*{$key}=.*/m";
        $line = $this->formatter->formatSetterLine($key, $value, $comment, $export);
        $this->buffer = preg_replace($pattern, $line, $this->buffer);

        return $this;
    }

    /**
     * Delete one setter in buffer
     *
     * @param  string $key
     *
     * @return DotenvWriter
     */
    public function deleteSetter($key)
    {
        $pattern = "/^(export\h)?\h*{$key}=.*\n/m";
        $this->buffer = preg_replace($pattern, null, $this->buffer);

        return $this;
    }

    /**
     * Save buffer to special file path
     *
     * @param  string $filePath
     *
     * @return DotenvWriter
     */
    public function save($filePath)
    {
        $this->ensureFileIsWritable($filePath);
        file_put_contents($filePath, $this->buffer);

        return $this;
    }
}


<?php namespace Jackiedo\DotenvEditor;

use Jackiedo\DotenvEditor\Contracts\DotenvFormatter as DotenvFormatterContract;
use Jackiedo\DotenvEditor\Contracts\DotenvReader as DotenvReaderContract;
use Jackiedo\DotenvEditor\Exceptions\UnableReadFileException;

/**
 * The DotenvReader class.
 *
 * @package Jackiedo\DotenvEditor
 * @author Jackie Do <anhvudo@gmail.com>
 */
class DotenvReader implements DotenvReaderContract
{
    /**
     * The file path
     *
     * @var string
     */
    protected $filePath;

    /**
     * Instance of Jackiedo\DotenvEditor\DotenvFormatter
     *
     * @var object
     */
    protected $formatter;

    /**
     * Create a new reader instance
     *
     * @param \Jackiedo\DotenvEditor\Contracts\DotenvFormatter $formatter
     */
    public function __construct(DotenvFormatterContract $formatter)
    {
        $this->formatter = $formatter;
    }

    /**
     * Load file
     *
     * @param  string $filePath
     *
     * @return DotenvReader
     */
    public function load($filePath)
    {
        $this->filePath = $filePath;
        return $this;
    }

    /**
     * Ensures the given file is readable.
     *
     * @throws \Jackiedo\DotenvEditor\Exceptions\UnableReadFileException
     *
     * @return void
     */
    protected function ensureFileIsReadable()
    {
        if (!is_readable($this->filePath) || !is_file($this->filePath)) {
            throw new UnableReadFileException(sprintf('Unable to read the file at %s.', $this->filePath));
        }
    }

    /**
     * Get content of file
     *
     * @return string
     */
    public function content()
    {
        $this->ensureFileIsReadable();

        return file_get_contents($this->filePath);
    }

    /**
     * Get informations of all lines from file content
     *
     * @return array
     */
    public function lines()
    {
        $content = [];
        $lines   = $this->readLinesFromFile();

        foreach ($lines as $row => $line) {
            $data = [
                'line'        => $row+1,
                'raw_data'    => $line,
                'parsed_data' => $this->formatter->parseLine($line)
            ];

            $content[] = $data;
        }

        return $content;
    }

    /**
     * Get informations of all keys from file content
     *
     * @return array
     */
    public function keys()
    {
        $content = [];
        $lines   = $this->readLinesFromFile();

        foreach ($lines as $row => $line) {
            $data = $this->formatter->parseLine($line);
            if ($data['type'] == 'setter') {
                $content[$data['key']] = [
                    'line'    => $row+1,
                    'export'  => $data['export'],
                    'value'   => $data['value'],
                    'comment' => $data['comment']
                ];
            }
        }

        return $content;
    }

    /**
     * Read content into an array of lines with auto-detected line endings
     *
     * @return array
     */
    protected function readLinesFromFile()
    {
        $this->ensureFileIsReadable();

        $autodetect = ini_get('auto_detect_line_endings');
        ini_set('auto_detect_line_endings', '1');
        $lines = file($this->filePath, FILE_IGNORE_NEW_LINES);
        ini_set('auto_detect_line_endings', $autodetect);

        return $lines;
    }
}


<?php namespace Jackiedo\DotenvEditor;

use Jackiedo\DotenvEditor\Contracts\DotenvFormatter as DotenvFormatterContract;
use Jackiedo\DotenvEditor\Exceptions\InvalidValueException;

/**
 * The .env formatter.
 *
 * @package Jackiedo\DotenvEditor
 * @author Jackie Do <anhvudo@gmail.com>
 */
class DotenvFormatter implements DotenvFormatterContract
{
    /**
     * Formatting the key of setter to writing
     *
     * @param   string  $key
     *
     * @return  string
     */
    public function formatKey($key)
    {
        return trim(str_replace(array('export ', '\'', '"', ' '), '', $key));
    }

    /**
     * Formatting the value of setter to writing
     *
     * @param   string  $value
     * @param   bool    $forceQuotes
     *
     * @return  string
     */
    public function formatValue($value, $forceQuotes = false)
    {
        if (!$forceQuotes && !preg_match('/[#\s"\'\\\\]|\\\\n/', $value)) {
            return $value;
        }

        $value = str_replace('\\', '\\\\', $value);
        $value = str_replace('"', '\"', $value);
        $value = "\"{$value}\"";

        return $value;
    }

    /**
     * Build an setter line from the individual components for writing
     *
     * @param string        $key
     * @param string|null   $value
     * @param string|null   $comment optional
     * @param bool          $export optional
     *
     * @return string
     */
    public function formatSetterLine($key, $value = null, $comment = null, $export = false)
    {
        $forceQuotes = (strlen($comment) > 0 && strlen(trim($value)) == 0);
        $value       = $this->formatValue($value, $forceQuotes);
        $key         = $this->formatKey($key);
        $comment     = $this->formatComment($comment);
        $export      = $export ? 'export ' : '';

        $line = "{$export}{$key}={$value}{$comment}";

        return $line;
    }

    /**
     * Formatting the comment to writing
     *
     * @param  string $comment
     *
     * @return string
     */
    public function formatComment($comment)
    {
        $comment = trim($comment, '# ');

        return (strlen($comment) > 0) ? " # {$comment}" : "";
    }

    /**
     * Normalising the key of setter to reading
     *
     * @param  string $key
     *
     * @return string
     */
    public function normaliseKey($key)
    {
        return $this->formatKey($key);
    }

    /**
     * Normalising the value of setter to reading
     *
     * @param  string $value
     * @param  string $quote
     *
     * @return string
     */
    public function normaliseValue($value, $quote = '')
    {
        if (strlen($quote) == 0) {
            return trim($value);
        }

        $value = str_replace("\\$quote", $quote, $value);
        $value = str_replace('\\\\', '\\', $value);
        return $value;
    }

    /**
     * Normalising the comment to reading
     *
     * @param  string $comment
     *
     * @return string
     */
    public function normaliseComment($comment)
    {
        return trim($comment, '# ');
    }

    /**
     * Parse a line into an array of type, export, key, value and comment
     *
     * @param  string $line
     *
     * @throws \Jackiedo\DotenvEditor\Exceptions\InvalidValueException
     *
     * @return array
     */
    public function parseLine($line)
    {
        $output = [
            'type'    => null,
            'export'  => null,
            'key'     => null,
            'value'   => null,
            'comment' => null,
        ];

        if ($this->isEmpty($line)) {
            $output['type'] = 'empty';
        } elseif ($this->isComment($line)) {
            $output['type']    = 'comment';
            $output['comment'] = $this->normaliseComment($line);
        } elseif ($this->looksLikeSetter($line)) {
            list($key, $data) = array_map('trim', explode('=', $line, 2));
            $export = $this->isExportKey($key);
            $key    = $this->normaliseKey($key);
            $data   = trim($data);

            if (!$data && $data !== '0') {
                $value   = '';
                $comment = '';
            } else {
                if ($this->beginsWithAQuote($data)) { // data starts with a quote
                    $quote = $data[0];
                    $regexPattern = sprintf(
                        '/^
                        %1$s          # match a quote at the start of the data
                        (             # capturing sub-pattern used
                         (?:          # we do not need to capture this
                          [^%1$s\\\\] # any character other than a quote or backslash
                          |\\\\\\\\   # or two backslashes together
                          |\\\\%1$s   # or an escaped quote e.g \"
                         )*           # as many characters that match the previous rules
                        )             # end of the capturing sub-pattern
                        %1$s          # and the closing quote
                        (.*)$         # and discard any string after the closing quote
                        /mx',
                        $quote
                    );

                    $value  = preg_replace($regexPattern, '$1', $data);
                    $extant = preg_replace($regexPattern, '$2', $data);

                    $value = $this->normaliseValue($value, $quote);
                    $comment = ($this->isComment($extant)) ? $this->normaliseComment($extant) : '';
                } else {
                    $parts   = explode(' #', $data, 2);

                    $value   = $this->normaliseValue($parts[0]);
                    $comment = (isset($parts[1])) ? $this->normaliseComment($parts[1]) : '';

                    // Unquoted values cannot contain whitespace
                    if (preg_match('/\s+/', $value) > 0) {
                        throw new InvalidValueException('Dotenv values containing spaces must be surrounded by quotes.');
                    }
                }
            }

            $output['type']    = 'setter';
            $output['export']  = $export;
            $output['key']     = $key;
            $output['value']   = $value;
            $output['comment'] = $comment;
        } else {
            $output['type'] = 'unknown';
        }

        return $output;
    }

    /**
     * Determine if the line in the file is empty line
     *
     * @param string $line
     *
     * @return bool
     */
    protected function isEmpty($line)
    {
        return strlen(trim($line)) == 0;
    }

    /**
     * Determine if the line in the file is a comment, e.g. begins with a #.
     *
     * @param string $line
     *
     * @return bool
     */
    protected function isComment($line)
    {
        return strpos(ltrim($line), '#') === 0;
    }

    /**
     * Determine if the given line looks like it's setting a key.
     *
     * @param string $line
     *
     * @return bool
     */
    protected function looksLikeSetter($line)
    {
        return strpos($line, '=') !== false && strpos($line, '=') !== 0;
    }

    /**
     * Determine if the given key begins with 'export '
     *
     * @param string $key
     *
     * @return bool
     */
    protected function isExportKey($key)
    {
        $pattern = '/^export\h.*$/';
        if (preg_match($pattern, trim($key))) {
            return true;
        }
        return false;
    }

    /**
     * Determine if the given string begins with a quote.
     *
     * @param string $data
     *
     * @return bool
     */
    protected function beginsWithAQuote($data)
    {
        return strpbrk($data[0], '"\'') !== false;
    }
}


<?php namespace Jackiedo\DotenvEditor;

use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Config\Repository as Config;
use Jackiedo\DotenvEditor\Exceptions\FileNotFoundException;
use Jackiedo\DotenvEditor\Exceptions\KeyNotFoundException;
use Jackiedo\DotenvEditor\Exceptions\NoBackupAvailableException;

/**
 * The DotenvEditor class.
 *
 * @package Jackiedo\DotenvEditor
 * @author Jackie Do <anhvudo@gmail.com>
 */
class DotenvEditor
{
    /**
     * The IoC Container
     *
     * @var \Illuminate\Container\Container
     */
    protected $app;

    /**
     * Store instance of Config Repository;
     *
     * @var \Illuminate\Config\Repository
     */
    protected $config;

    /**
     * The formatter instance
     *
     * @var \Jackiedo\DotenvEditor\DotenvFormatter
     */
    protected $formatter;

    /**
     * The reader instance
     *
     * @var \Jackiedo\DotenvEditor\DotenvReader
     */
    protected $reader;

    /**
     * The writer instance
     *
     * @var \Jackiedo\DotenvEditor\DotenvWriter
     */
    protected $writer;

    /**
     * The file path
     *
     * @var string
     */
    protected $filePath;

    /**
     * The auto backup status
     *
     * @var bool
     */
    protected $autoBackup;

    /**
     * The backup path
     *
     * @var string
     */
    protected $backupPath;

    /**
     * The backup filename prefix
     */
    const BACKUP_FILENAME_PREFIX = '.env.backup_';

    /**
     * The backup filename suffix
     */
    const BACKUP_FILENAME_SUFFIX = '';

    /**
     * Create a new DotenvEditor instance
     *
     * @param  \Illuminate\Contracts\Container\Container  $app
     * @param  \Illuminate\Contracts\Config\Repository    $config
     *
     * @return void
     */
    public function __construct(Container $app, Config $config)
    {
        $this->app       = $app;
        $this->config    = $config;
        $this->formatter = new DotenvFormatter;
        $this->reader    = new DotenvReader($this->formatter);
        $this->writer    = new DotenvWriter($this->formatter);

        $backupPath = $this->config->get('dotenv-editor.backupPath');

        if (is_null($backupPath)) {
            if (function_exists('base_path')) {
                $backupPath = base_path('storage/dotenv-editor/backups/');
            } else {
                $backupPath = __DIR__.'/../../../../../../storage/dotenv-editor/backups/';
            }
        }

        if (!is_dir($backupPath)) {
            mkdir($backupPath, 0777, true);
            copy(__DIR__ . '/../../stubs/gitignore.txt', $backupPath . '../.gitignore');
        }

        $this->backupPath = $backupPath;
        $this->autoBackup = $this->config->get('dotenv-editor.autoBackup', true);

        $this->load();
    }

    /**
     * Load file for working
     *
     * @param  string|null  $filePath           The file path
     * @param  boolean      $restoreIfNotFound  Restore this file from other file if it's not found
     * @param  string|null  $restorePath        The file path you want to restore from
     *
     * @return DotenvEditor
     */
    public function load($filePath = null, $restoreIfNotFound = false, $restorePath = null)
    {
        $this->resetContent();

        if (! is_null($filePath)) {
            $this->filePath = $filePath;
        } else {
            if (method_exists($this->app, 'environmentPath') && method_exists($this->app, 'environmentFile')) {
                $this->filePath = $this->app->environmentPath().'/'.$this->app->environmentFile();
            } else {
                $this->filePath = __DIR__.'/../../../../../../.env';
            }
        }

        $this->reader->load($this->filePath);

        if (file_exists($this->filePath)) {
            $this->writer->setBuffer($this->getContent());
            return $this;
        } elseif ($restoreIfNotFound) {
            return $this->restore($restorePath);
        } else {
            return $this;
        }
    }

    /**
     * Reset content for editor
     *
     * @return void
     */
    protected function resetContent()
    {
        $this->filePath = null;
        $this->reader->load(null);
        $this->writer->setBuffer(null);
    }

    /*
    |--------------------------------------------------------------------------
    | Working with reading
    |--------------------------------------------------------------------------
    |
    | getContent($content)
    | getLines()
    | getKeys()
    | keyExists($key)
    | getValue($key)
    |
    */

    /**
     * Get raw content of file
     *
     * @return string
     */
    public function getContent()
    {
        return $this->reader->content();
    }

    /**
     * Get all lines from file
     *
     * @return array
     */
    public function getLines()
    {
        return $this->reader->lines();
    }

    /**
     * Get all or exists given keys in file content
     *
     * @param  array  $keys
     *
     * @return array
     */
    public function getKeys($keys = [])
    {
        $allKeys = $this->reader->keys();

        return array_filter($allKeys, function ($key) use ($keys) {
            if (!empty($keys)) {
                return in_array($key, $keys);
            }
            return true;
        }, ARRAY_FILTER_USE_KEY);
    }

    /**
     * Check, if a given key is exists in the file content
     *
     * @param  string  $keys
     *
     * @return bool
     */
    public function keyExists($key)
    {
        $allKeys = $this->getKeys();
        if (array_key_exists($key, $allKeys)) {
            return true;
        }
        return false;
    }

    /**
     * Return the value matching to a given key in the file content
     *
     * @param  $key
     *
     * @throws \Jackiedo\DotenvEditor\Exceptions\KeyNotFoundException
     *
     * @return string
     */
    public function getValue($key)
    {
        $allKeys = $this->getKeys([$key]);
        if (array_key_exists($key, $allKeys)) {
            return $allKeys[$key]['value'];
        }
        throw new KeyNotFoundException('Requested key not found in your file.');
    }

    /*
    |--------------------------------------------------------------------------
    | Working with writing
    |--------------------------------------------------------------------------
    |
    | getBuffer()
    | addEmpty()
    | addComment($comment)
    | setKeys($data)
    | setKey($key, $value = null, $comment = null, $export = false)
    | deleteKeys($keys = [])
    | deleteKey($key)
    | save()
    |
    */

    /**
     * Return content in buffer
     *
     * @return string
     */
    public function getBuffer()
    {
        return $this->writer->getBuffer();
    }

    /**
     * Add empty line to buffer
     *
     * @return DotenvEditor
     */
    public function addEmpty()
    {
        $this->writer->appendEmptyLine();
        return $this;
    }

    /**
     * Add comment line to buffer
     *
     * @param object
     */
    public function addComment($comment)
    {
        $this->writer->appendCommentLine($comment);
        return $this;
    }

    /**
     * Set many keys to buffer
     *
     * @param  array  $data
     *
     * @return DotenvEditor
     */
    public function setKeys($data)
    {
        foreach ($data as $setter) {
            if (array_key_exists('key', $setter)) {
                $key     = $this->formatter->formatKey($setter['key']);
                $value   = array_key_exists('value', $setter) ? $setter['value'] : null;
                $comment = array_key_exists('comment', $setter) ? $setter['comment'] : null;
                $export  = array_key_exists('export', $setter) ? $setter['export'] : false;

                if (!is_file($this->filePath) || !$this->keyExists($key)) {
                    $this->writer->appendSetter($key, $value, $comment, $export);
                } else {
                    $oldInfo = $this->getKeys([$key]);
                    $comment = is_null($comment) ? $oldInfo[$key]['comment'] : $comment;
                    $this->writer->updateSetter($key, $value, $comment, $export);
                }
            }
        }

        return $this;
    }

    /**
     * Set one key to buffer
     *
     * @param string       $key      Key name of setter
     * @param string|null  $value    Value of setter
     * @param string|null  $comment  Comment of setter
     * @param boolean      $export   Leading key name by "export "
     *
     * @return DotenvEditor
     */
    public function setKey($key, $value = null, $comment = null, $export = false)
    {
        $data = [compact('key', 'value', 'comment', 'export')];

        return $this->setKeys($data);
    }

    /**
     * Delete many keys in buffer
     *
     * @param  array $keys
     *
     * @return DotenvEditor
     */
    public function deleteKeys($keys = [])
    {
        foreach ($keys as $key) {
            $this->writer->deleteSetter($key);
        }

        return $this;
    }

    /**
     * Delete on key in buffer
     *
     * @param  string  $key
     *
     * @return DotenvEditor
     */
    public function deleteKey($key)
    {
        $keys = [$key];

        return $this->deleteKeys($keys);
    }

    /**
     * Save buffer to file
     *
     * @return DotenvEditor
     */
    public function save()
    {
        if (is_file($this->filePath) && $this->autoBackup) {
            $this->backup();
        }

        $this->writer->save($this->filePath);
        return $this;
    }

    /*
    |--------------------------------------------------------------------------
    | Working with backups
    |--------------------------------------------------------------------------
    |
    | autoBackup($on)
    | backup()
    | getBackups()
    | getLatestBackup()
    | restore($filePath = null)
    | deleteBackups($filePaths = [])
    | deleteBackup($filePath)
    |
    */

    /**
     * Switching of the auto backup mode
     *
     * @param  boolean  $on
     *
     * @return DotenvEditor
     */
    public function autoBackup($on = true)
    {
        $this->autoBackup = $on;
        return $this;
    }

    /**
     * Create one backup of loaded file
     *
     * @return DotenvEditor
     */
    public function backup()
    {
        if (!is_file($this->filePath)) {
            throw new FileNotFoundException("File does not exist at path {$this->filePath}");
            return false;
        }

        copy(
            $this->filePath,
            $this->backupPath . self::BACKUP_FILENAME_PREFIX . date('Y_m_d_His') . self::BACKUP_FILENAME_SUFFIX
        );

        return $this;
    }

    /**
     * Return an array with all available backups
     *
     * @return array
     */
    public function getBackups()
    {
        $backups = array_diff(scandir($this->backupPath), array('..', '.'));
        $output = [];

        foreach ($backups as $backup) {
            $filenamePrefix = preg_quote(self::BACKUP_FILENAME_PREFIX, '/');
            $filenameSuffix = preg_quote(self::BACKUP_FILENAME_SUFFIX, '/');
            $filenameRegex  = '/^' .$filenamePrefix. '(\d{4})_(\d{2})_(\d{2})_(\d{2})(\d{2})(\d{2})' .$filenameSuffix. '$/';

            $datetime = preg_replace($filenameRegex, '$1-$2-$3 $4:$5:$6', $backup);

            $data = [
                'filename'   => $backup,
                'filepath'   => $this->backupPath . $backup,
                'created_at' => $datetime,
            ];

            $output[] = $data;
        }

        return $output;
    }

    /**
     * Return the information of the latest backup file
     *
     * @return array
     */
    public function getLatestBackup()
    {
        $backups = $this->getBackups();

        if (empty($backups)) {
            return null;
        }

        $latestBackup = 0;
        foreach ($backups as $backup) {
            $timestamp = strtotime($backup['created_at']);
            if ($timestamp > $latestBackup) {
                $latestBackup = $timestamp;
            }
        }

        $fileName  = self::BACKUP_FILENAME_PREFIX . date("Y_m_d_His", $latestBackup) . self::BACKUP_FILENAME_SUFFIX;
        $filePath  = $this->backupPath . $fileName;
        $createdAt = date("Y-m-d H:i:s", $latestBackup);

        $output = [
            'filename'   => $fileName,
            'filepath'   => $filePath,
            'created_at' => $createdAt
        ];

        return $output;
    }

    /**
     * Restore the loaded file from latest backup file or from special file.
     *
     * @param  string|null  $filePath
     *
     * @return DotenvEditor
     */
    public function restore($filePath = null)
    {
        if (is_null($filePath)) {
            $latestBackup = $this->getLatestBackup();
            if (is_null($latestBackup)) {
                throw new NoBackupAvailableException("There are no available backups!");
            }
            $filePath = $latestBackup['filepath'];
        }

        if (!is_file($filePath)) {
            throw new FileNotFoundException("File does not exist at path {$filePath}");
        }

        copy($filePath, $this->filePath);
        $this->writer->setBuffer($this->getContent());

        return $this;
    }

    /**
     * Delete all or the given backup files
     *
     * @param  array  $filePaths
     *
     * @return DotenvEditor
     */
    public function deleteBackups($filePaths = [])
    {
        if (empty($filePaths)) {
            $allBackups = $this->getBackups();
            foreach ($allBackups as $backup) {
                $filePaths[] = $backup['filepath'];
            }
        }

        foreach ($filePaths as $filePath) {
            if (is_file($filePath)) {
                unlink($filePath);
            }
        }
        return $this;
    }

    /**
     * Delete the given backup file
     *
     * @param  string  $filePath
     *
     * @return DotenvEditor
     */
    public function deleteBackup($filePath)
    {
        return $this->deleteBackups([$filePath]);
    }
}


<?php namespace Jackiedo\DotenvEditor;

use Illuminate\Support\ServiceProvider;

/**
 * DotenvEditorServiceProvider
 *
 * @package Jackiedo\DotenvEditor
 * @author Jackie Do <anhvudo@gmail.com>
 */
class DotenvEditorServiceProvider extends ServiceProvider
{

    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        /**
         * Loading and publishing package's config
         */
        $packageConfigPath = __DIR__ . '/../../config/config.php';
        $appConfigPath     = config_path('dotenv-editor.php');

        $this->mergeConfigFrom($packageConfigPath, 'dotenv-editor');

        $this->publishes([
            $packageConfigPath => $appConfigPath,
        ], 'config');
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('dotenv-editor', 'Jackiedo\DotenvEditor\DotenvEditor');

        $this->registerCommands();
    }

    /**
     * Register commands
     *
     * @return void
     */
    protected function registerCommands()
    {
        $this->app->bind('command.dotenv.backup', 'Jackiedo\DotenvEditor\Console\Commands\DotenvBackupCommand');
        $this->app->bind('command.dotenv.deletekey', 'Jackiedo\DotenvEditor\Console\Commands\DotenvDeleteKeyCommand');
        $this->app->bind('command.dotenv.getbackups', 'Jackiedo\DotenvEditor\Console\Commands\DotenvGetBackupsCommand');
        $this->app->bind('command.dotenv.getkeys', 'Jackiedo\DotenvEditor\Console\Commands\DotenvGetKeysCommand');
        $this->app->bind('command.dotenv.restore', 'Jackiedo\DotenvEditor\Console\Commands\DotenvRestoreCommand');
        $this->app->bind('command.dotenv.setkey', 'Jackiedo\DotenvEditor\Console\Commands\DotenvSetKeyCommand');

        $this->commands('command.dotenv.backup');
        $this->commands('command.dotenv.deletekey');
        $this->commands('command.dotenv.getbackups');
        $this->commands('command.dotenv.getkeys');
        $this->commands('command.dotenv.restore');
        $this->commands('command.dotenv.setkey');
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [
            'dotenv-editor',
            'command.dotenv.backup',
            'command.dotenv.deletekey',
            'command.dotenv.getbackups',
            'command.dotenv.getkeys',
            'command.dotenv.restore',
            'command.dotenv.setkey'
        ];
    }
}


<?php  namespace Jackiedo\DotenvEditor\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Jackiedo\DotenvEditor\Console\Traits\CreateCommandInstanceTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class DotenvDeleteKeyCommand extends Command
{
    use ConfirmableTrait, CreateCommandInstanceTrait;

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'dotenv:delete-key';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Delete one setter in the .env file';

    /**
     * The .env file path
     *
     * @var string|null
     */
    protected $filePath = null;

    /**
     * The key name use to add or update
     *
     * @var string
     */
    protected $key;

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function fire()
    {
        $this->transferInputsToProperties();

        if (! $this->confirmToProceed()) {
            return false;
        }

        $this->line('Deleting key in your file...');

        $this->editor->load($this->filePath)->deleteKey($this->key)->save();

        $this->info("The key [{$this->key}] is deletted successfully.");
    }

    /**
     * Transfer inputs to properties of editing
     *
     * @return void
     */
    protected function transferInputsToProperties()
    {
        $filePath = $this->stringToType($this->option('filepath'));

        $this->filePath = (is_string($filePath)) ? base_path($filePath) : null;
        $this->key      = $this->argument('key');
    }

    /**
     * Convert string to corresponding type
     *
     * @param  string $string
     *
     * @return mixed
     */
    protected function stringToType($string)
    {
        if (is_string($string)) {
            switch (true) {
                case ($string == 'null' || $string == 'NULL'):
                    $string = null;
                    break;

                case ($string == 'true' || $string == 'TRUE'):
                    $string = true;
                    break;

                case ($string == 'false' || $string == 'FALSE'):
                    $string = false;
                    break;

                default:
                    break;
            }
        }

        return $string;
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return [
            array('key', InputArgument::REQUIRED, 'Key name will be deleted.')
        ];
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            array('filepath', null, InputOption::VALUE_OPTIONAL, 'The file path should use to load for working. Do not use if you want to load file .env at root application folder.'),
            array('force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.')
        ];
    }
}


<?php
namespace PhpUnitsOfMeasure;

class UnitOfMeasure implements UnitOfMeasureInterface
{
    /**
     * For the special case of units that have a linear conversion factor, this factory
     * method simplifies the construction of the unit of measure.
     *
     * For example the relationship between meters and feet is a simple multiplicative factor of
     * 0.3048 meters in a foot.  Converting back and forth between these two units is a matter of
     * multiplication or division by this scaling factor.
     *
     * In contrast, converting Celsius to Fahrenheit involves an offset calculation, and cannot
     * be represented by a simple conversion factor.  In such cases this class's constructor should be
     * invoked directly.
     *
     * To help in getting the multiplication and division right, think of the toNativeUnitFactor
     * as the number you'd multiply this unit by to get to the native unit of measure.  In
     * other words:
     * 'Value in the native unit of measure' = 'Value in this unit of measure' * toNativeUnitFactor
     *
     * @param string $name               This unit of measure's canonical name
     * @param float  $toNativeUnitFactor The factor to scale the unit by where factor * base unit = this unit
     *
     * @return self
     */
    public static function linearUnitFactory($name, $toNativeUnitFactor)
    {
        return new static(
            $name,
            function ($valueInNativeUnit) use ($toNativeUnitFactor) {
                return $valueInNativeUnit / $toNativeUnitFactor;
            },
            function ($valueInThisUnit) use ($toNativeUnitFactor) {
                return $valueInThisUnit * $toNativeUnitFactor;
            }
        );
    }

    /**
     * This is a special case of the linear unit factory above, for use in generating the native unit of measure
     * for a given physical quantity.  By definition, the conversion factor is 1.
     *
     * @param string $name This unit of measure's canonical name
     *
     * @return self
     */
    public static function nativeUnitFactory($name)
    {
        return static::linearUnitFactory($name, 1);
    }

    /**
     * The canonical name for this unit of measure.
     *
     * Typically this is the official way the unit is abbreviated.
     *
     * @var string
     */
    protected $name;

    /**
     * A collection of alias names that map to this unit of measure
     *
     * @var string[]
     */
    protected $aliases = [];

    /**
     * A callable that can convert a value in this quantity's
     * native unit to this unit of measure.
     *
     * @var callable
     */
    protected $fromNativeUnit;

    /**
     * A callable that can convert a value in this unit of measure
     * to a value in the native unit of the physical quantity.
     *
     * @var callable
     */
    protected $toNativeUnit;

    /**
     * Configure this object's mandatory properties.
     *
     * @param string   $name           This unit of measure's canonical name
     * @param callable $fromNativeUnit The callable that can cast values into this unit of measure from the native unit of measure
     * @param callable $toNativeUnit   The callable that can cast values into the native unit from this unit of measure
     *
     * @throws Exception\NonStringUnitName
     */
    public function __construct($name, callable $fromNativeUnit, callable $toNativeUnit)
    {
        if (!is_string($name)) {
            throw new Exception\NonStringUnitName([':name' => $name]);
        }

        $this->name           = $name;
        $this->fromNativeUnit = $fromNativeUnit;
        $this->toNativeUnit   = $toNativeUnit;
    }

    /**
     * @see \PhpUnitsOfMeasure\UnitOfMeasureInterface::getName
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @see \PhpUnitsOfMeasure\UnitOfMeasureInterface::addAlias
     */
    public function addAlias($alias)
    {
        if (!is_string($alias)) {
            throw new Exception\NonStringUnitName([':name' => $alias]);
        }

        $this->aliases[] = $alias;
    }

    /**
     * @see \PhpUnitsOfMeasure\UnitOfMeasureInterface::getAliases
     */
    public function getAliases()
    {
        return $this->aliases;
    }

    /**
     * @see \PhpUnitsOfMeasure\UnitOfMeasureInterface::isAliasOf
     */
    public function isAliasOf($unit)
    {
        if (!is_string($unit)) {
            throw new Exception\NonStringUnitName([':name' => $unit]);
        }

        return in_array($unit, $this->aliases);
    }

    /**
     * @see \PhpUnitsOfMeasure\UnitOfMeasureInterface::convertValueFromNativeUnitOfMeasure
     */
    public function convertValueFromNativeUnitOfMeasure($value)
    {
        if (!is_numeric($value)) {
            throw new Exception\NonNumericValue([':value' => $value]);
        }

        $callable = $this->fromNativeUnit;
        return $callable($value);
    }

    /**
     * @see \PhpUnitsOfMeasure\UnitOfMeasureInterface::convertValueToNativeUnitOfMeasure
     */
    public function convertValueToNativeUnitOfMeasure($value)
    {
        if (!is_numeric($value)) {
            throw new Exception\NonNumericValue([':value' => $value]);
        }

        $callable = $this->toNativeUnit;
        return $callable($value);
    }
}


<?php
namespace PhpUnitsOfMeasure;

abstract class AbstractPhysicalQuantity implements PhysicalQuantityInterface
{
    /**
     * The collection of units in which this quantity can be represented.
     *
     * Commented out to ensure that each child class defines its own instance of this static.
     *
     * @var UnitOfMeasureInterface[]
     */
    // protected static $unitDefinitions;

    /**
     * Static cache for unit lookups.
     *
     * @var UnitOfMeasureInterface[]
     */
    private static $unitCache = [];

    /**
     * Create a cache key for the unit lookup cache.
     *
     * @var UnitOfMeasureInterface[]
     */
    private static function buildUnitCacheKey($unit)
    {
        return get_called_class() . '#' . $unit;
    }

    /**
     * Register a new unit of measure for all instances of this this physical quantity.
     *
     * @throws Exception\DuplicateUnitNameOrAlias If the unit name or any alias already exists
     *
     * @param UnitOfMeasureInterface $unit The new unit of measure
     */
    public static function addUnit(UnitOfMeasureInterface $unit)
    {
        if (static::unitNameOrAliasesAlreadyRegistered($unit)) {
            throw new Exception\DuplicateUnitNameOrAlias([
                ':labels' => implode(', ', array_merge([$unit->getName()], $unit->getAliases()))
            ]);
        }

        static::$unitDefinitions[] = $unit;
    }

    /**
     * Get the unit of measure that matches the given name by either name or alias.
     *
     * @param string $unit A name or alias by which the unit is known.
     *
     * @throws Exception\UnknownUnitOfMeasure when an unknown unit of measure is given
     *
     * @return UnitOfMeasureInterface
     */
    public static function getUnit($unit)
    {
        // If this class hasn't been initialized yet, do so now
        if (!is_array(static::$unitDefinitions)) {
            static::$unitDefinitions = [];
            static::initialize();
        }

        $key = static::buildUnitCacheKey($unit);
        if (isset(self::$unitCache[$key])) {
            return self::$unitCache[$key];
        }

        foreach (static::$unitDefinitions as $unitOfMeasure) {
            if ($unit === $unitOfMeasure->getName() || $unitOfMeasure->isAliasOf($unit)) {
                return self::$unitCache[$key] = $unitOfMeasure;
            }
        }

        throw new Exception\UnknownUnitOfMeasure([':unit' => $unit]);
    }

    /**
     * Given a unit of measure, determine if its name or any of its aliases conflict
     * with the set of already-known unit names and aliases.
     *
     * @param UnitOfMeasureInterface $unit The unit in question
     *
     * @return boolean true if there is a conflict, false if there is not
     */
    protected static function unitNameOrAliasesAlreadyRegistered(UnitOfMeasureInterface $unit)
    {
        // If this class hasn't been initialized yet, do so now
        if (!is_array(static::$unitDefinitions)) {
            static::$unitDefinitions = [];
            static::initialize();
        }

        $currentUnitNamesAndAliases = [];
        foreach (static::$unitDefinitions as $unitOfMeasure) {
            $currentUnitNamesAndAliases[] = $unitOfMeasure->getName();
            $currentUnitNamesAndAliases = array_merge($currentUnitNamesAndAliases, $unitOfMeasure->getAliases());
        }

        $newUnitNamesAndAliases = array_merge([$unit->getName()], $unit->getAliases());

        return count(array_intersect($currentUnitNamesAndAliases, $newUnitNamesAndAliases)) > 0;
    }

    /**
     * Initialize the static properties of this quantity class, such as the set of
     * default units of measure.
     */
    protected static function initialize()
    {
    }

    /**
     * The scalar value, in the original unit of measure.
     *
     * @var float
     */
    protected $originalValue;

    /**
     * The original unit of measure's string representation.
     *
     * @var string
     */
    protected $originalUnit;

    /**
     * Store the value and its original unit.
     *
     * @param float  $value The scalar value of the measurement
     * @param string $unit  The unit of measure in which this value is provided
     *
     * @throws Exception\NonNumericValue If the value is not numeric
     * @throws Exception\NonStringUnitName If the unit is not a string
     */
    public function __construct($value, $unit)
    {
        if (!is_numeric($value)) {
            throw new Exception\NonNumericValue([':value' => $value]);
        }

        if (!is_string($unit)) {
            throw new Exception\NonStringUnitName([':name' => $unit]);
        }

        $this->originalValue = $value;
        $this->originalUnit  = $unit;
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::toUnit
     */
    public function toUnit($toUnit)
    {
        return $this->toUnitOfMeasure(static::getUnit($toUnit));
    }

    /**
     * Convert this quantity to the given unit of measure.
     *
     * @param UnitOfMeasureInterface $unit The object representing the target unit of measure.
     *
     * @return float This quantity's value in the given unit of measure.
     */
    private function toUnitOfMeasure(UnitOfMeasureInterface $unit)
    {
        $thisValueInNativeUnit = $this->toNativeUnit();
        return $unit->convertValueFromNativeUnitOfMeasure($thisValueInNativeUnit);
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::toNativeUnit
     */
    public function toNativeUnit()
    {
        return static::getUnit($this->originalUnit)
            ->convertValueToNativeUnitOfMeasure($this->originalValue);
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::__toString
     */
    public function __toString()
    {
        return trim($this->originalValue . ' ' . static::getUnit($this->originalUnit)->getName());
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::add
     */
    public function add(PhysicalQuantityInterface $quantity)
    {
        if (!$this->isEquivalentQuantity($quantity)) {
            throw new Exception\PhysicalQuantityMismatch([
                ':lhs' => (string) $this,
                ':rhs' => (string) $quantity
            ]);
        }

        $quantityValueInThisOriginalUnit = $quantity->toUnitOfMeasure(static::getUnit($this->originalUnit));
        $newValue = $this->originalValue + $quantityValueInThisOriginalUnit;

        return new static($newValue, static::getUnit($this->originalUnit)->getName());
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::subtract
     */
    public function subtract(PhysicalQuantityInterface $quantity)
    {
        if (!$this->isEquivalentQuantity($quantity)) {
            throw new Exception\PhysicalQuantityMismatch([
                ':lhs' => (string) $this,
                ':rhs' => (string) $quantity
            ]);
        }

        $quantityValueInThisOriginalUnit = $quantity->toUnitOfMeasure(static::getUnit($this->originalUnit));
        $newValue = $this->originalValue - $quantityValueInThisOriginalUnit;

        return new static($newValue, static::getUnit($this->originalUnit)->getName());
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::isEquivalentQuantity
     */
    public function isEquivalentQuantity(PhysicalQuantityInterface $testQuantity)
    {
        return get_class($this) === get_class($testQuantity);
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::isUnitDefined
     */
    public static function isUnitDefined($name)
    {
        $units = static::getUnitDefinitions();
        foreach ($units as $unit) {
            if ($name === $unit->getName() || $unit->isAliasOf($name)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @see \PhpUnitsOfMeasure\PhysicalQuantityInterface::listAllUnits
     */
    public static function listAllUnits()
    {
        $return = [];
        $units = static::getUnitDefinitions();
        foreach ($units as $unit) {
            $return[$unit->getName()] = $unit->getAliases();
        }
        return $return;
    }

    /**
     * Get the unit definition array
     * @return Array $unitDefinitions
     */
    public static function getUnitDefinitions()
    {
        if (!is_array(static::$unitDefinitions)) {
            static::$unitDefinitions = [];
            static::initialize();
        }

        return static::$unitDefinitions;
    }
}


<?php
namespace PhpUnitsOfMeasure;

/**
 * Physical quantities with this trait
 * have units which are metric and therefore have
 * a standard set of prefixes.
 */
trait HasSIUnitsTrait
{
    /**
     * Given the patterns for generating the names and aliases,
     * generate the various metric units of measure and add
     * them to this physical quantity.
     *
     * Names and Aliases are created by replacing identifiers in the respective
     * patterns.  The current allowed replacement identifiers are:
     *   %p = the abbreviated SI prefix, like 'M' for 'megameter' or 'k' for 'kilogram'
     *   %P = the full SI prefix, like 'mega' for 'megameter' or 'kilo' for 'kilogram'
     *   %U = uppercase version of %P
     *
     * So for instance, in order to generate 'kg', 'mg', and 'g' names for SI units, the
     * appropriate pattern would be '%pg'.  Similarly, to generate 'kilogram', 'milligram',
     * and 'gram' aliases, the pattern would be '%Pgram'.
     *
     * The $siUnit given in the 1st parameter must be some SI unit in the series of units
     * to be generated by this method.  This value is necessary to establish a conversion
     * factor between this continuum of SI units and the Physical Quantity's native unit.
     *
     * The second parameter provides a scaling factor between the given SI unit in the first parameter
     * and the base unit of the SI continuum (ie, 'grams', 'meters', 'seconds', etc).  For instance,
     * if a Kilogram unit of measure was passed for the 1st parameter, it would be necessary to
     * then pass 1e-3 in the 2nd parameter to indicate that a gram is 1/1000 of the given unit.
     *
     * @param  UnitOfMeasure $siUnit             A unit in this physical quantity that is an SI unit of measure
     * @param  integer       $toBaseSiUnitFactor The power-of-ten factor that converts the given SI unit into the not-prefixed SI base unit (ie 1e-3 for kilograms)
     * @param  string        $namePattern        The pattern to apply to the base unit's name to generate a new SI unit name
     * @param  array         $aliasPatterns      The collection of alias patterns to use in generating a new SI unit's aliases
     */
    protected static function addMissingSIPrefixedUnits(
        UnitOfMeasure $siUnit,
        $toBaseSiUnitFactor,
        $namePattern,
        array $aliasPatterns = []
    ) {
        /**
         * The standard set of SI prefixes
         */
        $siPrefixes = [
            [
                'abbr_prefix' => 'Y',
                'long_prefix' => 'yotta',
                'factor'      => 1e24
            ],
            [
                'abbr_prefix' => 'Z',
                'long_prefix' => 'zetta',
                'factor'      => 1e21
            ],
            [
                'abbr_prefix' => 'E',
                'long_prefix' => 'exa',
                'factor'      => 1e18
            ],
            [
                'abbr_prefix' => 'P',
                'long_prefix' => 'peta',
                'factor'      => 1e15
            ],
            [
                'abbr_prefix' => 'T',
                'long_prefix' => 'tera',
                'factor'      => 1e12
            ],
            [
                'abbr_prefix' => 'G',
                'long_prefix' => 'giga',
                'factor'      => 1e9
            ],
            [
                'abbr_prefix' => 'M',
                'long_prefix' => 'mega',
                'factor'      => 1e6
            ],
            [
                'abbr_prefix' => 'k',
                'long_prefix' => 'kilo',
                'factor'      => 1e3
            ],
            [
                'abbr_prefix' => 'h',
                'long_prefix' => 'hecto',
                'factor'      => 1e2
            ],
            [
                'abbr_prefix' => 'da',
                'long_prefix' => 'deca',
                'factor'      => 1e1
            ],
            [
                'abbr_prefix' => '',
                'long_prefix' => '',
                'factor'      => 1
            ],
            [
                'abbr_prefix' => 'd',
                'long_prefix' => 'deci',
                'factor'      => 1e-1
            ],
            [
                'abbr_prefix' => 'c',
                'long_prefix' => 'centi',
                'factor'      => 1e-2
            ],
            [
                'abbr_prefix' => 'm',
                'long_prefix' => 'milli',
                'factor'      => 1e-3
            ],
            [
                'abbr_prefix' => 'µ',
                'long_prefix' => 'micro',
                'factor'      => 1e-6
            ],
            [
                'abbr_prefix' => 'n',
                'long_prefix' => 'nano',
                'factor'      => 1e-9
            ],
            [
                'abbr_prefix' => 'p',
                'long_prefix' => 'pico',
                'factor'      => 1e-12
            ],
            [
                'abbr_prefix' => 'f',
                'long_prefix' => 'femto',
                'factor'      => 1e-15
            ],
            [
                'abbr_prefix' => 'a',
                'long_prefix' => 'atto',
                'factor'      => 1e-18
            ],
            [
                'abbr_prefix' => 'z',
                'long_prefix' => 'zepto',
                'factor'      => 1e-21
            ],
            [
                'abbr_prefix' => 'y',
                'long_prefix' => 'yocto',
                'factor'      => 1e-24
            ],
        ];

        // Determine the conversion factor from the no-prefix SI unit to the physical quantity's native unit
        $noPrefixToNativeUnitFactor = $siUnit->convertValueToNativeUnitOfMeasure(1) * $toBaseSiUnitFactor;

        // For each of the standard SI prefixes, attempt to register a new unit of measure
        foreach ($siPrefixes as $prefixDefinition) {
            // Build a function for resolving a pattern into a unit name
            $parsePattern = function ($pattern) use ($prefixDefinition) {
                return strtr(
                    $pattern,
                    [
                        '%p' => $prefixDefinition['abbr_prefix'],
                        '%P' => $prefixDefinition['long_prefix'],
                        '%U' => strtoupper($prefixDefinition['long_prefix'])
                    ]
                );
            };

            // Generate the base name of the new unit
            $name = $parsePattern($namePattern);

            // Determine the factor that converts the new unit into the physical quantity's
            //   native unit of measure.
            $toNativeUnitFactor = $noPrefixToNativeUnitFactor * $prefixDefinition['factor'];

            // Instantiate the new unit of measure
            $newUnit = UnitOfMeasure::linearUnitFactory($name, $toNativeUnitFactor);

            // Generate the aliases of the new unit
            foreach ($aliasPatterns as $aliasPattern) {
                $newUnitAlias = $parsePattern($aliasPattern);
                $newUnit->addAlias($newUnitAlias);
            }

            // If the unit doesn't conflict with any of the already-existing units, register it
            if (!static::unitNameOrAliasesAlreadyRegistered($newUnit)) {
                static::addUnit($newUnit);
            }
        }
    }
}