1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.6.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Widget;
16:
17: use Cake\Core\App;
18: use Cake\Core\Configure\Engine\PhpConfig;
19: use Cake\View\StringTemplate;
20: use Cake\View\View;
21: use ReflectionClass;
22: use RuntimeException;
23:
24: /**
25: * A registry/factory for input widgets.
26: *
27: * Can be used by helpers/view logic to build form widgets
28: * and other HTML widgets.
29: *
30: * This class handles the mapping between names and concrete classes.
31: * It also has a basic name based dependency resolver that allows
32: * widgets to depend on each other.
33: *
34: * Each widget should expect a StringTemplate instance as their first
35: * argument. All other dependencies will be included after.
36: *
37: * Widgets can ask for the current view by using the `_view` widget.
38: */
39: class WidgetLocator
40: {
41:
42: /**
43: * Array of widgets + widget configuration.
44: *
45: * @var array
46: */
47: protected $_widgets = [];
48:
49: /**
50: * Templates to use.
51: *
52: * @var \Cake\View\StringTemplate
53: */
54: protected $_templates;
55:
56: /**
57: * Constructor
58: *
59: * @param \Cake\View\StringTemplate $templates Templates instance to use.
60: * @param \Cake\View\View $view The view instance to set as a widget.
61: * @param string|array $widgets See add() method for more information.
62: */
63: public function __construct(StringTemplate $templates, View $view, $widgets = [])
64: {
65: $this->_templates = $templates;
66: if (!empty($widgets)) {
67: $this->add($widgets);
68: foreach ($this->_widgets as $key => $widget) {
69: if (is_string($widget) && !class_exists($widget)) {
70: $this->load($widget);
71: unset($this->_widgets[$key]);
72: }
73: }
74: }
75: $this->_widgets['_view'] = $view;
76: }
77:
78: /**
79: * Load a config file containing widgets.
80: *
81: * Widget files should define a `$config` variable containing
82: * all the widgets to load. Loaded widgets will be merged with existing
83: * widgets.
84: *
85: * @param string $file The file to load
86: * @return void
87: */
88: public function load($file)
89: {
90: $loader = new PhpConfig();
91: $widgets = $loader->read($file);
92: $this->add($widgets);
93: }
94:
95: /**
96: * Adds or replaces existing widget instances/configuration with new ones.
97: *
98: * Widget arrays can either be descriptions or instances. For example:
99: *
100: * ```
101: * $registry->add([
102: * 'label' => new MyLabelWidget($templates),
103: * 'checkbox' => ['Fancy.MyCheckbox', 'label']
104: * ]);
105: * ```
106: *
107: * The above shows how to define widgets as instances or as
108: * descriptions including dependencies. Classes can be defined
109: * with plugin notation, or fully namespaced class names.
110: *
111: * @param array $widgets Array of widgets to use.
112: * @return void
113: * @throws \RuntimeException When class does not implement WidgetInterface.
114: */
115: public function add(array $widgets)
116: {
117: foreach ($widgets as $object) {
118: if (is_object($object) &&
119: !($object instanceof WidgetInterface)
120: ) {
121: throw new RuntimeException(
122: 'Widget objects must implement Cake\View\Widget\WidgetInterface.'
123: );
124: }
125: }
126: $this->_widgets = $widgets + $this->_widgets;
127: }
128:
129: /**
130: * Get a widget.
131: *
132: * Will either fetch an already created widget, or create a new instance
133: * if the widget has been defined. If the widget is undefined an instance of
134: * the `_default` widget will be returned. An exception will be thrown if
135: * the `_default` widget is undefined.
136: *
137: * @param string $name The widget name to get.
138: * @return \Cake\View\Widget\WidgetInterface widget interface class.
139: * @throws \RuntimeException when widget is undefined.
140: * @throws \ReflectionException
141: */
142: public function get($name)
143: {
144: if (!isset($this->_widgets[$name]) && empty($this->_widgets['_default'])) {
145: throw new RuntimeException(sprintf('Unknown widget "%s"', $name));
146: }
147: if (!isset($this->_widgets[$name])) {
148: $name = '_default';
149: }
150: $this->_widgets[$name] = $this->_resolveWidget($this->_widgets[$name]);
151:
152: return $this->_widgets[$name];
153: }
154:
155: /**
156: * Clear the registry and reset the widgets.
157: *
158: * @return void
159: */
160: public function clear()
161: {
162: $this->_widgets = [];
163: }
164:
165: /**
166: * Resolves a widget spec into an instance.
167: *
168: * @param mixed $widget The widget to get
169: * @return \Cake\View\Widget\WidgetInterface
170: * @throws \RuntimeException when class cannot be loaded or does not implement WidgetInterface.
171: * @throws \ReflectionException
172: */
173: protected function _resolveWidget($widget)
174: {
175: $type = gettype($widget);
176: if ($type === 'object') {
177: return $widget;
178: }
179:
180: if ($type === 'string') {
181: $widget = [$widget];
182: }
183:
184: $class = array_shift($widget);
185: $className = App::className($class, 'View/Widget', 'Widget');
186: if ($className === false || !class_exists($className)) {
187: throw new RuntimeException(sprintf('Unable to locate widget class "%s"', $class));
188: }
189: if ($type === 'array' && count($widget)) {
190: $reflection = new ReflectionClass($className);
191: $arguments = [$this->_templates];
192: foreach ($widget as $requirement) {
193: $arguments[] = $this->get($requirement);
194: }
195: $instance = $reflection->newInstanceArgs($arguments);
196: } else {
197: $instance = new $className($this->_templates);
198: }
199: if (!($instance instanceof WidgetInterface)) {
200: throw new RuntimeException(sprintf('"%s" does not implement the WidgetInterface', $className));
201: }
202:
203: return $instance;
204: }
205: }
206: