├── .gitignore
├── registration.php
├── view
└── adminhtml
│ ├── web
│ ├── js
│ │ └── configscopehints.js
│ └── css
│ │ └── configscopehints.less
│ └── layout
│ └── adminhtml_system_config_edit.xml
├── etc
├── module.xml
└── di.xml
├── composer.json
├── LICENSE
├── Plugin
└── Framework
│ └── Data
│ └── Form
│ └── Element
│ └── Fieldset.php
├── README.md
└── Helper
└── Data.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
--------------------------------------------------------------------------------
/registration.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/etc/di.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/view/adminhtml/layout/adminhtml_system_config_edit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ericthehacker/magento2-configscopehints",
3 | "description": "Magento 2 store config override hints module",
4 | "require": {
5 | "magento/framework": "*",
6 | "magento/module-config": "*"
7 | },
8 | "type": "magento2-module",
9 | "version": "3.1.1",
10 | "autoload": {
11 | "files": [ "registration.php" ],
12 | "psr-4": {
13 | "EW\\ConfigScopeHints\\": ""
14 | }
15 | },
16 | "authors": [
17 | {
18 | "name": "Eric Wiese",
19 | "homepage": "https://ericwie.se/",
20 | "role": "Developer"
21 | },
22 | {
23 | "name": "Erik Hansen",
24 | "homepage": "https://www.classyllama.com/",
25 | "role": "Developer"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Eric Wiese
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/view/adminhtml/web/css/configscopehints.less:
--------------------------------------------------------------------------------
1 | .overridden-hint-wrapper {
2 | font-style: italic;
3 |
4 | .lead-text {
5 | &:before {
6 | color: #eb5202;
7 | content: '\e623';
8 | font-family: "Admin Icons";
9 | display: inline-block;
10 | margin-right: 0.5em;
11 | }
12 | }
13 |
14 | .override-scope {
15 | cursor: pointer;
16 | line-height: 40px; //improve touch experience
17 | position: relative;
18 | padding-right: 4rem; //prevent arrow from overlapping text
19 |
20 | &:before {
21 | font-family: "Admin Icons";
22 | font-style: normal;
23 | content: '\e616';
24 | font-size: 1.8rem;
25 | position: absolute;
26 | right: 1.3rem;
27 | }
28 |
29 | &.open {
30 | border-bottom: none; //remove native accordion border so following .override-value can have it
31 |
32 | &:before {
33 | content: '\e615'; //set icon to close arrow
34 | }
35 | }
36 | }
37 |
38 | .override-value {
39 | &.visible {
40 | display: block;
41 | }
42 |
43 | span.override-value-hint-label {
44 | display: block;
45 | margin-bottom: 1rem;
46 | }
47 |
48 | //move native accordion left whitespace from margin to padding
49 | margin-left: 0;
50 | padding-left: 40px;
51 | }
52 | }
--------------------------------------------------------------------------------
/Plugin/Framework/Data/Form/Element/Fieldset.php:
--------------------------------------------------------------------------------
1 | helper = $helper;
29 | $this->request = $request;
30 | }
31 |
32 | /**
33 | * Check for indexes of $config that must be present
34 | * for current override detection logic.
35 | *
36 | * @param array $config
37 | * @return bool
38 | */
39 | protected function isConfigValid(array $config) {
40 | return isset($config['field_config'])
41 | && isset($config['field_config']['path'])
42 | && isset($config['field_config']['id'])
43 | && isset($config['scope'])
44 | && isset($config['scope_id']);
45 | }
46 |
47 | /**
48 | * If field is overwritten at more specific scope(s),
49 | * set field hint with this info.
50 | *
51 | * @param OriginalFieldset $subject
52 | * @param callable $proceed
53 | * @param string $elementId
54 | * @param string $type
55 | * @param array $config
56 | * @param bool $after
57 | * @param bool $isAdvanced
58 | * @return \Magento\Framework\Data\Form\Element\AbstractElement
59 | */
60 | public function aroundAddField(OriginalFieldset $subject, callable $proceed, $elementId, $type, $config, $after = false, $isAdvanced = false) {
61 | if($this->isConfigValid($config)) {
62 | $path = $config['field_config']['path'] . '/' . $config['field_config']['id'];
63 | $scope = $config['scope'];
64 | $scopeId = $config['scope_id'];
65 | $section = $this->request->getParam('section'); //@todo: don't talk to request directly
66 |
67 | $overriddenLevels = $this->helper->getOverriddenLevels(
68 | $path,
69 | $scope,
70 | $scopeId
71 | );
72 |
73 | if(!empty($overriddenLevels)) {
74 | $config['comment'] .= $this->helper->formatOverriddenScopes($section, $overriddenLevels);
75 | }
76 | }
77 |
78 | return $proceed($elementId, $type, $config, $after, $isAdvanced);
79 | }
80 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EW_ConfigScopeHints
2 |
3 | This module shows information in the Store Configuration backend UI when a config field is overridden at more specific scope(s), along with information about these scope(s).
4 |
5 | ## Installation
6 |
7 | This module can be installed manually or by using Composer (recommended).
8 |
9 | ### Composer Installation
10 |
11 | Each of these commands should be run from the command line at the Magento 2 root.
12 |
13 | ```bash
14 | # add this repository to your composer.json
15 | $ composer config repositories.magento2-configscopehints git https://github.com/ericthehacker/magento2-configscopehints.git
16 |
17 | # require module
18 | $ composer require ericthehacker/magento2-configscopehints
19 |
20 | # enable module
21 | $ php -f bin/magento module:enable EW_ConfigScopeHints
22 | $ php -f bin/magento setup:upgrade
23 | ```
24 |
25 | ### Manual Installation
26 |
27 | First, download contents of this repo into `app/code/EW/ConfigScopeHints` using a command similar to the following in the Magento 2 root.
28 |
29 | ```bash
30 | $ mkdir -p app/code/EW # create vendor directory
31 | $ wget https://github.com/ericthehacker/magento2-configscopehints/archive/master.zip # download zip of module contents
32 | $ unzip master.zip -d app/code/EW # unzip module into vendor directory
33 | $ mv app/code/EW/magento2-configscopehints-master app/code/EW/ConfigScopeHints # correct directory name
34 | $ rm master.zip # clean up zip file
35 | ```
36 |
37 | Finally, enable module by running the following from the command line at the Magento 2 root.
38 |
39 | ```bash
40 | $ php -f bin/magento module:enable EW_ConfigScopeHints
41 | $ php -f bin/magento setup:upgrade
42 | ```
43 |
44 | Sit back and enjoy!
45 |
46 | ## Usage
47 |
48 | After installing the module, when viewing a system configuration field, an alert icon and message will be shown below the field value if it has been overridden at a more specific scope.
49 |
50 | The icon is only shown when the value is overridden at a more specific scope than the current one – that is, if viewing at the default scope, overrides at the website or store view level are shown, but if viewing at the website level, only overrides below the currently selected website are shown.
51 |
52 | Along with the alert message, a detailed list of the exact scope(s) that override the value, with links directly to the store config for the current section at those scopes. Clicking an override hint row arrow will expand the row to also show the field's value at that scope.
53 |
54 | 
55 |
56 | ## Compatibility and Technical Notes
57 |
58 | As of version 3.0.0 of this module has been tested against Magento 2.1.x. It's likely compatible with 2.0.x as well, but this is untested.
59 |
60 | > NOTE: For known compatibility with 2.0.x, check out version [2.1.0][2.1.0] of the module.
61 |
62 | ## Known Issues
63 |
64 | ### MAGETWO-62648
65 |
66 | When used on Magento 2.1.3, the module can produce a false positive when viewing a website scope. If a given config value has been overridden at this website scope, any children store views which have "Use Website" set for the value will incorrectly show as being overridden.
67 |
68 | This is a known [core bug][MAGETWO-62648].
69 |
70 | ### Non-standard Fieldset Renderers
71 |
72 | Store config groups which use non-standard fieldset renderers are currently ignored. Of the native store config fields, the following exhibit this trait.
73 |
74 | * Advanced -> Advanced -> Disable Modules Output
75 | * Sales -> Payment Methods
76 |
77 |
78 | [2.1.0]: https://github.com/ericthehacker/magento2-configscopehints/releases/tag/v2.1.0
79 | [MAGETWO-62648]: https://github.com/magento/magento2/issues/7943
--------------------------------------------------------------------------------
/Helper/Data.php:
--------------------------------------------------------------------------------
1 | storeManager = $storeManager;
51 | $this->context = $context;
52 | // Ideally we would just retrieve the urlBuilder using $this->content->getUrlBuilder(), but since it retrieves
53 | // an instance of \Magento\Framework\Url instead of \Magento\Backend\Model\Url, we must explicitly request it
54 | // via DI.
55 | $this->urlBuilder = $urlBuilder;
56 | $this->configStructure = $configStructure;
57 | $this->escaper = $escaper;
58 | }
59 |
60 | /**
61 | * Gets store tree in a format easily walked over
62 | * for config path value comparison
63 | *
64 | * @return array
65 | */
66 | public function getScopeTree() {
67 | $tree = array(self::WEBSITE_SCOPE_CODE => array());
68 |
69 | $websites = $this->storeManager->getWebsites();
70 |
71 | /* @var $website Website */
72 | foreach($websites as $website) {
73 | $tree[self::WEBSITE_SCOPE_CODE][$website->getId()] = array(self::STORE_VIEW_SCOPE_CODE => array());
74 |
75 | /* @var $store Store */
76 | foreach($website->getStores() as $store) {
77 | $tree[self::WEBSITE_SCOPE_CODE][$website->getId()][self::STORE_VIEW_SCOPE_CODE][] = $store->getId();
78 | }
79 | }
80 |
81 | return $tree;
82 | }
83 |
84 | /**
85 | * Wrapper method to get config value at path, scope, and scope code provided
86 | *
87 | * @param string $path
88 | * @param string $contextScope
89 | * @param string|int $contextScopeId
90 | * @return string
91 | */
92 | protected function _getConfigValue($path, $contextScope, $contextScopeId) {
93 | return $this->context->getScopeConfig()->getValue($path, $contextScope, $contextScopeId);
94 | }
95 |
96 | /**
97 | * Gets human-friendly display value(s) for given config path
98 | *
99 | * @param string $path
100 | * @param string $contextScope
101 | * @param string|int $contextScopeId
102 | * @return array
103 | */
104 | public function getConfigDisplayValue($path, $contextScope, $contextScopeId) {
105 | $value = $this->_getConfigValue($path, $contextScope, $contextScopeId);
106 |
107 | $labels = [$value]; //default labels to raw value
108 |
109 | /** @var \Magento\Config\Model\Config\Structure\Element\Field $field */
110 | $field = $this->configStructure->getElement($path);
111 |
112 | if($field->getOptions()) {
113 | $labels = []; //reset labels so we can add human-friendly labels
114 |
115 | $optionsByValue = [];
116 | foreach($field->getOptions() as $option) {
117 | $optionsByValue[$option['value']] = $option;
118 | }
119 |
120 | $values = explode(',', $value);
121 |
122 | foreach($values as $valueInstance) {
123 | $labels[] = isset($optionsByValue[$valueInstance])
124 | ? $optionsByValue[$valueInstance]['label'] : $valueInstance;
125 |
126 | }
127 | }
128 |
129 | return $labels;
130 | }
131 |
132 | /**
133 | * Gets array of scopes and scope IDs where path value is different
134 | * than supplied context scope and context scope ID.
135 | * If no lower-level scopes override the value, return empty array.
136 | *
137 | * @param $path
138 | * @param $contextScope
139 | * @param $contextScopeId
140 | * @return array
141 | */
142 | public function getOverriddenLevels($path, $contextScope, $contextScopeId) {
143 | $tree = $this->getScopeTree();
144 |
145 | $currentValue = $this->_getConfigValue($path, $contextScope, $contextScopeId);
146 |
147 | $overridden = array();
148 |
149 | switch($contextScope) {
150 | case self::WEBSITE_SCOPE_CODE:
151 | $stores = array_values($tree[self::WEBSITE_SCOPE_CODE][$contextScopeId][self::STORE_VIEW_SCOPE_CODE]);
152 | foreach($stores as $storeId) {
153 | $value = $this->_getConfigValue($path, self::STORE_VIEW_SCOPE_CODE, $storeId);
154 | if($value != $currentValue) {
155 | $overridden[] = array(
156 | 'scope' => 'store',
157 | 'scope_id' => $storeId,
158 | 'value' => $value,
159 | 'display_value' => $this->getConfigDisplayValue($path, self::STORE_VIEW_SCOPE_CODE, $storeId)
160 | );
161 | }
162 | }
163 | break;
164 | case 'default':
165 | foreach($tree[self::WEBSITE_SCOPE_CODE] as $websiteId => $website) {
166 | $websiteValue = $this->_getConfigValue($path, self::WEBSITE_SCOPE_CODE, $websiteId);
167 | if($websiteValue != $currentValue) {
168 | $overridden[] = array(
169 | 'scope' => 'website',
170 | 'scope_id' => $websiteId,
171 | 'value' => $websiteValue,
172 | 'display_value' => $this->getConfigDisplayValue($path, self::WEBSITE_SCOPE_CODE, $websiteId)
173 | );
174 | }
175 |
176 | foreach($website[self::STORE_VIEW_SCOPE_CODE] as $storeId) {
177 | $value = $this->_getConfigValue($path, self::STORE_VIEW_SCOPE_CODE, $storeId);
178 | if($value != $currentValue && $value != $websiteValue) {
179 | $overridden[] = array(
180 | 'scope' => 'store',
181 | 'scope_id' => $storeId,
182 | 'value' => $value,
183 | 'display_value' => $this->getConfigDisplayValue($path, self::STORE_VIEW_SCOPE_CODE, $storeId)
184 | );
185 | }
186 | }
187 | }
188 | break;
189 | }
190 |
191 | return $overridden;
192 | }
193 |
194 | /**
195 | * Get HTML formatted value label(s)
196 | *
197 | * @param array $labels
198 | * @return string
199 | */
200 | protected function getFormattedValueLabels(array $labels) {
201 | if(count($labels) == 1) {
202 | //if only one value, simply return it
203 | return '' .
204 | nl2br($this->escaper->escapeHtml($labels[0])) .
205 | '';
206 | }
207 |
208 | $formattedLabels = '';
209 |
210 | foreach($labels as $label) {
211 | $formattedLabels .= '
' .
212 | nl2br($this->escaper->escapeHtml($label)) .
213 | '';
214 | }
215 |
216 | return '';
217 | }
218 |
219 | /**
220 | * Get HTML output for override hint UI
221 | *
222 | * @param string $section
223 | * @param array $overridden
224 | * @return string
225 | */
226 | public function formatOverriddenScopes($section, array $overridden) {
227 | $formatted = '' .
228 | '
' . __('This config field is overridden at the following scope(s):') . '
' .
229 | '
';
230 |
231 | foreach($overridden as $overriddenScope) {
232 | $scope = $overriddenScope['scope'];
233 | $scopeId = $overriddenScope['scope_id'];
234 | $value = $overriddenScope['value'];
235 | $valueLabel = $overriddenScope['display_value'];
236 | $scopeLabel = $scopeId;
237 |
238 | $url = '#';
239 | switch($scope) {
240 | case 'website':
241 | $url = $this->urlBuilder->getUrl(
242 | '*/*/*',
243 | array(
244 | 'section' => $section,
245 | 'website' => $scopeId
246 | )
247 | );
248 | $scopeLabel = __(
249 | 'Website %2',
250 | $url,
251 | $this->storeManager->getWebsite($scopeId)->getName()
252 | );
253 |
254 | break;
255 | case 'store':
256 | /** @var \Magento\Store\Model\Store $store */
257 | $store = $this->storeManager->getStore($scopeId);
258 | $website = $store->getWebsite();
259 | $url = $this->urlBuilder->getUrl(
260 | '*/*/*',
261 | array(
262 | 'section' => $section,
263 | 'store' => $store->getId()
264 | )
265 | );
266 | $scopeLabel = __(
267 | 'Store view %2',
268 | $url,
269 | $website->getName() . ' / ' . $store->getName()
270 | );
271 | break;
272 | }
273 |
274 | $formatted .=
275 | '- '
276 | . $scopeLabel .
277 | '
' .
278 | '- ' . $this->getFormattedValueLabels($valueLabel) . '
';
279 | }
280 |
281 | $formatted .= '
';
282 |
283 | return $formatted;
284 | }
285 | }
286 |
--------------------------------------------------------------------------------