PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / API / Proxy.php
matomo / app / core / API Last commit date
DataTableManipulator 1 year ago ApiRenderer.php 1 year ago CORSHandler.php 1 year ago DataTableGenericFilter.php 1 year ago DataTableManipulator.php 1 year ago DataTablePostProcessor.php 1 year ago DocumentationGenerator.php 1 year ago Inconsistencies.php 2 years ago NoDefaultValue.php 2 years ago Proxy.php 1 year ago Request.php 1 year ago ResponseBuilder.php 1 year ago
Proxy.php
615 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\API;
10
11 use Exception;
12 use Piwik\Http\BadRequestException;
13 use Piwik\Common;
14 use Piwik\Container\StaticContainer;
15 use Piwik\Context;
16 use Piwik\Piwik;
17 use Piwik\Plugin\API;
18 use Piwik\Plugin\Manager;
19 use ReflectionClass;
20 use ReflectionMethod;
21 // prevent upgrade error eg from Matomo 3.x to Matomo 4.x. Refs https://github.com/matomo-org/matomo/pull/16468
22 // the `false` is important otherwise it would fail and try to load the proxy.php file again.
23 if (!class_exists('Piwik\\API\\NoDefaultValue', \false)) {
24 // phpcs:ignoreFile PSR1.Classes.ClassDeclaration.MultipleClasses
25 class NoDefaultValue
26 {
27 }
28 }
29 /**
30 * Proxy is a singleton that has the knowledge of every method available, their parameters
31 * and default values.
32 * Proxy receives all the API calls requests via call() and forwards them to the right
33 * object, with the parameters in the right order.
34 *
35 * It will also log the performance of API calls (time spent, parameter values, etc.) if logger available
36 */
37 class Proxy
38 {
39 // array of already registered plugins names
40 protected $alreadyRegistered = array();
41 protected $metadataArray = array();
42 private $hideIgnoredFunctions = \true;
43 // when a parameter doesn't have a default value we use this
44 private $noDefaultValue;
45 public function __construct()
46 {
47 $this->noDefaultValue = new \Piwik\API\NoDefaultValue();
48 }
49 public static function getInstance()
50 {
51 return StaticContainer::get(self::class);
52 }
53 /**
54 * Returns array containing reflection meta data for all the loaded classes
55 * eg. number of parameters, method names, etc.
56 *
57 * @return array
58 */
59 public function getMetadata()
60 {
61 ksort($this->metadataArray);
62 return $this->metadataArray;
63 }
64 /**
65 * Registers the API information of a given module.
66 *
67 * The module to be registered must be
68 * - a singleton (providing a getInstance() method)
69 * - the API file must be located in plugins/ModuleName/API.php
70 * for example plugins/Referrers/API.php
71 *
72 * The method will introspect the methods, their parameters, etc.
73 *
74 * @param string $className ModuleName eg. "API"
75 */
76 public function registerClass($className)
77 {
78 if (isset($this->alreadyRegistered[$className])) {
79 return;
80 }
81 $this->includeApiFile($className);
82 $this->checkClassIsSingleton($className);
83 $rClass = new ReflectionClass($className);
84 if (!$this->shouldHideAPIMethod($rClass->getDocComment())) {
85 foreach ($rClass->getMethods() as $method) {
86 $this->loadMethodMetadata($className, $method);
87 }
88 $this->setDocumentation($rClass, $className);
89 $this->alreadyRegistered[$className] = \true;
90 }
91 }
92 /**
93 * Will be displayed in the API page
94 *
95 * @param ReflectionClass $rClass Instance of ReflectionClass
96 * @param string $className Name of the class
97 */
98 private function setDocumentation($rClass, $className)
99 {
100 // Doc comment
101 $doc = $rClass->getDocComment();
102 $doc = str_replace(" * " . \PHP_EOL, "<br>", $doc);
103 // boldify the first line only if there is more than one line, otherwise too much bold
104 if (substr_count($doc, '<br>') > 1) {
105 $firstLineBreak = strpos($doc, "<br>");
106 $doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>"));
107 }
108 $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc);
109 $doc = preg_replace("/(@method).*/", "", $doc);
110 $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc);
111 // replace 'foo' and `bar` and "foobar" with code blocks... much magic
112 $doc = preg_replace('/`(.*?)`/', '<code>$1</code>', $doc);
113 $this->metadataArray[$className]['__documentation'] = $doc;
114 }
115 /**
116 * Returns number of classes already loaded
117 * @return int
118 */
119 public function getCountRegisteredClasses()
120 {
121 return count($this->alreadyRegistered);
122 }
123 /**
124 * Will execute $className->$methodName($parametersValues)
125 * If any error is detected (wrong number of parameters, method not found, class not found, etc.)
126 * it will throw an exception
127 *
128 * It also logs the API calls, with the parameters values, the returned value, the performance, etc.
129 * You can enable logging in config/global.ini.php (log_api_call)
130 *
131 * @param string $className The class name (eg. API)
132 * @param string $methodName The method name
133 * @param array $parametersRequest The parameters pairs (name=>value)
134 *
135 * @return mixed|null
136 * @throws Exception|\Piwik\NoAccessException
137 */
138 public function call($className, $methodName, $parametersRequest)
139 {
140 // Temporarily sets the Request array to this API call context
141 return Context::executeWithQueryParameters($parametersRequest, function () use($className, $methodName, $parametersRequest) {
142 $this->registerClass($className);
143 $request = new \Piwik\Request($parametersRequest);
144 /**
145 * instantiate the object
146 * @var API $object
147 */
148 $object = $className::getInstance();
149 // check method exists
150 $this->checkMethodExists($className, $methodName);
151 // get the list of parameters required by the method
152 $parameterNamesDefaultValuesAndTypes = $this->getParametersListWithTypes($className, $methodName);
153 // load parameters in the right order, etc.
154 if ($object->usesAutoSanitizeInputParams() && !$this->usesUnsanitizedInputParams($className, $methodName)) {
155 $finalParameters = $this->getSanitizedRequestParametersArray($parameterNamesDefaultValuesAndTypes, $request->getParameters());
156 } else {
157 $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValuesAndTypes, $request);
158 }
159 // allow plugins to manipulate the value
160 $pluginName = $this->getModuleNameFromClassName($className);
161 $returnedValue = null;
162 /**
163 * Triggered before an API request is dispatched.
164 *
165 * This event can be used to modify the arguments passed to one or more API methods.
166 *
167 * **Example**
168 *
169 * Piwik::addAction('API.Request.dispatch', function (&$parameters, $pluginName, $methodName) {
170 * if ($pluginName == 'Actions') {
171 * if ($methodName == 'getPageUrls') {
172 * // ... do something ...
173 * } else {
174 * // ... do something else ...
175 * }
176 * }
177 * });
178 *
179 * @param array &$finalParameters List of parameters that will be passed to the API method.
180 * @param string $pluginName The name of the plugin the API method belongs to.
181 * @param string $methodName The name of the API method that will be called.
182 */
183 Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName));
184 /**
185 * Triggered before an API request is dispatched.
186 *
187 * This event exists for convenience and is triggered directly after the {@hook API.Request.dispatch}
188 * event is triggered. It can be used to modify the arguments passed to a **single** API method.
189 *
190 * _Note: This is can be accomplished with the {@hook API.Request.dispatch} event as well, however
191 * event handlers for that event will have to do more work._
192 *
193 * **Example**
194 *
195 * Piwik::addAction('API.Actions.getPageUrls', function (&$parameters) {
196 * // force use of a single website. for some reason.
197 * $parameters['idSite'] = 1;
198 * });
199 *
200 * @param array &$finalParameters List of parameters that will be passed to the API method.
201 */
202 Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters));
203 /**
204 * Triggered before an API request is dispatched.
205 *
206 * Use this event to intercept an API request and execute your own code instead. If you set
207 * `$returnedValue` in a handler for this event, the original API method will not be executed,
208 * and the result will be what you set in the event handler.
209 *
210 * @param mixed &$returnedValue Set this to set the result and preempt normal API invocation.
211 * @param array &$finalParameters List of parameters that will be passed to the API method.
212 * @param string $pluginName The name of the plugin the API method belongs to.
213 * @param string $methodName The name of the API method that will be called.
214 * @param array $parametersRequest The query parameters for this request.
215 */
216 Piwik::postEvent('API.Request.intercept', [&$returnedValue, $finalParameters, $pluginName, $methodName, $parametersRequest]);
217 $apiParametersInCorrectOrder = array();
218 foreach ($parameterNamesDefaultValuesAndTypes as $name => $parameter) {
219 if (isset($finalParameters[$name]) || array_key_exists($name, $finalParameters)) {
220 $apiParametersInCorrectOrder[] = $finalParameters[$name];
221 }
222 }
223 // call the method if a hook hasn't already set an output variable
224 if ($returnedValue === null) {
225 $returnedValue = call_user_func_array(array($object, $methodName), $apiParametersInCorrectOrder);
226 }
227 $endHookParams = array(&$returnedValue, array('className' => $className, 'module' => $pluginName, 'action' => $methodName, 'parameters' => $finalParameters));
228 /**
229 * Triggered directly after an API request is dispatched.
230 *
231 * This event exists for convenience and is triggered immediately before the
232 * {@hook API.Request.dispatch.end} event. It can be used to modify the output of a **single**
233 * API method.
234 *
235 * _Note: This can be accomplished with the {@hook API.Request.dispatch.end} event as well,
236 * however event handlers for that event will have to do more work._
237 *
238 * **Example**
239 *
240 * // append (0 hits) to the end of row labels whose row has 0 hits
241 * Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
242 * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
243 * if ($hits === 0) {
244 * return $label . " (0 hits)";
245 * } else {
246 * return $label;
247 * }
248 * }, null, array('nb_hits'));
249 * }
250 *
251 * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
252 * {@link Piwik\DataTable DataTable} instance.
253 * could be a {@link Piwik\DataTable DataTable}.
254 * @param array $extraInfo An array holding information regarding the API request. Will
255 * contain the following data:
256 *
257 * - **className**: The namespace-d class name of the API instance
258 * that's being called.
259 * - **module**: The name of the plugin the API request was
260 * dispatched to.
261 * - **action**: The name of the API method that was executed.
262 * - **parameters**: The array of parameters passed to the API
263 * method.
264 */
265 Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams);
266 /**
267 * Triggered directly after an API request is dispatched.
268 *
269 * This event can be used to modify the output of any API method.
270 *
271 * **Example**
272 *
273 * // append (0 hits) to the end of row labels whose row has 0 hits for any report that has the 'nb_hits' metric
274 * Piwik::addAction('API.Actions.getPageUrls.end', function (&$returnValue, $info)) {
275 * // don't process non-DataTable reports and reports that don't have the nb_hits column
276 * if (!($returnValue instanceof DataTableInterface)
277 * || in_array('nb_hits', $returnValue->getColumns())
278 * ) {
279 * return;
280 * }
281 *
282 * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
283 * if ($hits === 0) {
284 * return $label . " (0 hits)";
285 * } else {
286 * return $label;
287 * }
288 * }, null, array('nb_hits'));
289 * }
290 *
291 * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
292 * {@link Piwik\DataTable DataTable} instance.
293 * @param array $extraInfo An array holding information regarding the API request. Will
294 * contain the following data:
295 *
296 * - **className**: The namespace-d class name of the API instance
297 * that's being called.
298 * - **module**: The name of the plugin the API request was
299 * dispatched to.
300 * - **action**: The name of the API method that was executed.
301 * - **parameters**: The array of parameters passed to the API
302 * method.
303 */
304 Piwik::postEvent('API.Request.dispatch.end', $endHookParams);
305 return $returnedValue;
306 });
307 }
308 /**
309 * Returns the parameters names and default values for the method $name
310 * of the class $class
311 *
312 * @param string $class The class name
313 * @param string $name The method name
314 * @return array Format array(
315 * 'testParameter' => null, // no default value
316 * 'life' => 42, // default value = 42
317 * 'date' => 'yesterday',
318 * );
319 */
320 public function getParametersList($class, $name)
321 {
322 return array_combine(array_keys($this->metadataArray[$class][$name]['parameters']), array_column($this->metadataArray[$class][$name]['parameters'], 'default'));
323 }
324 /**
325 * Returns the parameters names, default values and types for the method $name
326 * of the class $class
327 *
328 * @param string $class The class name
329 * @param string $name The method name
330 * @return array Format array(
331 * 'testParameter' => ['default' => null, 'type' => null], // no default value
332 * 'life' => ['default' => 42, 'type' => 'int'], // default value 42, type hint is int
333 * );
334 */
335 public function getParametersListWithTypes($class, $name)
336 {
337 return $this->metadataArray[$class][$name]['parameters'];
338 }
339 /**
340 * Check if given method name is deprecated or not.
341 */
342 public function isDeprecatedMethod($class, $methodName)
343 {
344 return $this->metadataArray[$class][$methodName]['isDeprecated'] ?? \false;
345 }
346 /**
347 * Check if given method uses unsanitized input parameters.
348 */
349 public function usesUnsanitizedInputParams($class, $methodName)
350 {
351 return $this->metadataArray[$class][$methodName]['unsanitizedInputParams'] ?? \false;
352 }
353 /**
354 * Returns the 'moduleName' part of '\\Piwik\\Plugins\\moduleName\\API'
355 *
356 * @param string $className "API"
357 * @return string "Referrers"
358 */
359 public function getModuleNameFromClassName($className)
360 {
361 return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className);
362 }
363 public function isExistingApiAction($pluginName, $apiAction)
364 {
365 $namespacedApiClassName = "\\Piwik\\Plugins\\{$pluginName}\\API";
366 $api = $namespacedApiClassName::getInstance();
367 return method_exists($api, $apiAction);
368 }
369 public function buildApiActionName($pluginName, $apiAction)
370 {
371 return sprintf("%s.%s", $pluginName, $apiAction);
372 }
373 /**
374 * Sets whether to hide '@ignore'd functions from method metadata or not.
375 *
376 * @param bool $hideIgnoredFunctions
377 */
378 public function setHideIgnoredFunctions($hideIgnoredFunctions)
379 {
380 $this->hideIgnoredFunctions = $hideIgnoredFunctions;
381 // make sure metadata gets reloaded
382 $this->alreadyRegistered = array();
383 $this->metadataArray = array();
384 }
385 /**
386 * Returns an array containing the *sanitized* values of the parameters to pass to the method to call
387 *
388 * @param array $requiredParameters array of (parameter name, default value)
389 * @param array $parametersRequest
390 * @throws Exception
391 * @return array values to pass to the function call
392 */
393 private function getSanitizedRequestParametersArray($requiredParameters, $parametersRequest)
394 {
395 $finalParameters = [];
396 foreach ($requiredParameters as $name => $parameter) {
397 try {
398 $defaultValue = $parameter['default'];
399 $type = $parameter['type'];
400 $request = new \Piwik\Request($parametersRequest);
401 if (in_array($name, ['segment', 'password', 'passwordConfirmation']) && !empty($parametersRequest[$name])) {
402 // special handling for some parameters:
403 // segment: we do not want to sanitize user input as it would break the segment encoding
404 // password / passwordConfirmation: sanitizing this parameters might change special chars in passwords, breaking login and confirmation boxes
405 $requestValue = $parametersRequest[$name];
406 } elseif ($defaultValue instanceof \Piwik\API\NoDefaultValue) {
407 if ($type === 'bool') {
408 $requestValue = $request->getBoolParameter($name);
409 } else {
410 $requestValue = Common::getRequestVar($name, null, $type, $parametersRequest);
411 }
412 } else {
413 try {
414 if ($type === 'bool') {
415 $requestValue = $request->getBoolParameter($name, $defaultValue);
416 } else {
417 $requestValue = Common::getRequestVar($name, $defaultValue, $type, $parametersRequest);
418 }
419 } catch (Exception $e) {
420 // Special case: empty parameter in the URL, should return the empty string
421 if (isset($parametersRequest[$name]) && $parametersRequest[$name] === '') {
422 $requestValue = '';
423 } else {
424 $requestValue = $defaultValue;
425 }
426 }
427 }
428 } catch (Exception $e) {
429 throw new Exception(Piwik::translate('General_PleaseSpecifyValue', array($name)));
430 }
431 $finalParameters[$name] = $requestValue;
432 }
433 return $finalParameters;
434 }
435 /**
436 * Returns an array containing the values of the parameters to pass to the method to call
437 *
438 * @param array $requiredParameters array of (parameter name, default value)
439 * @param \Piwik\Request $request
440 * @throws Exception
441 * @return array values to pass to the function call
442 */
443 private function getRequestParametersArray($requiredParameters, \Piwik\Request $request) : array
444 {
445 $finalParameters = [];
446 foreach ($requiredParameters as $name => $parameter) {
447 try {
448 $defaultValue = $parameter['default'];
449 $type = $parameter['type'] ?? '';
450 $requestValue = null;
451 switch (strtolower($type)) {
452 case 'bool':
453 $method = 'getBoolParameter';
454 break;
455 case 'int':
456 $method = 'getIntegerParameter';
457 break;
458 case 'string':
459 $method = 'getStringParameter';
460 break;
461 case 'float':
462 $method = 'getFloatParameter';
463 break;
464 case 'array':
465 $method = 'getArrayParameter';
466 break;
467 default:
468 $method = 'getParameter';
469 }
470 if ($defaultValue instanceof \Piwik\API\NoDefaultValue) {
471 $requestValue = $request->{$method}($name);
472 } elseif ($defaultValue === null) {
473 try {
474 $requestValue = $request->{$method}($name);
475 } catch (\InvalidArgumentException $e) {
476 $requestValue = null;
477 }
478 } else {
479 $requestValue = $request->{$method}($name, $defaultValue);
480 }
481 } catch (Exception $e) {
482 throw new Exception(Piwik::translate('General_PleaseSpecifyValue', [$name]));
483 }
484 $finalParameters[$name] = $requestValue;
485 }
486 return $finalParameters;
487 }
488 /**
489 * Includes the class API by looking up plugins/xxx/API.php
490 *
491 * @param string $fileName api class name eg. "API"
492 * @throws Exception
493 */
494 private function includeApiFile($fileName)
495 {
496 $module = self::getModuleNameFromClassName($fileName);
497 $path = Manager::getPluginDirectory($module) . '/API.php';
498 if (is_readable($path)) {
499 require_once $path;
500 // prefixed by PIWIK_INCLUDE_PATH
501 } else {
502 throw new Exception("API module {$module} not found.");
503 }
504 }
505 /**
506 * @param string $class name of a class
507 * @param ReflectionMethod $method instance of ReflectionMethod
508 */
509 private function loadMethodMetadata($class, $method)
510 {
511 if (!$this->checkIfMethodIsAvailable($method)) {
512 return;
513 }
514 $name = $method->getName();
515 $parameters = $method->getParameters();
516 $docComment = $method->getDocComment();
517 $aParameters = array();
518 foreach ($parameters as $parameter) {
519 $nameVariable = $parameter->getName();
520 $defaultValue = $this->noDefaultValue;
521 if ($parameter->isDefaultValueAvailable()) {
522 $defaultValue = $parameter->getDefaultValue();
523 }
524 $type = $parameter->getType();
525 // In case no default value is defined in the method, but the type hint allows null, we assume null as default value
526 if ($type && $type->allowsNull() && $defaultValue === $this->noDefaultValue) {
527 $defaultValue = null;
528 }
529 $aParameters[$nameVariable] = ['default' => $defaultValue, 'type' => $type && $type->isBuiltin() ? $type->getName() : null, 'allowsNull' => $type ? $type->allowsNull() : $defaultValue === null];
530 }
531 $this->metadataArray[$class][$name]['parameters'] = $aParameters;
532 $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters();
533 $this->metadataArray[$class][$name]['isDeprecated'] = \false !== strstr($docComment, '@deprecated');
534 $this->metadataArray[$class][$name]['unsanitizedInputParams'] = \false !== strstr($docComment, '@unsanitized');
535 }
536 /**
537 * Checks that the method exists in the class
538 *
539 * @param string $className The class name
540 * @param string $methodName The method name
541 * @throws Exception If the method is not found
542 */
543 private function checkMethodExists($className, $methodName)
544 {
545 if (!$this->isMethodAvailable($className, $methodName)) {
546 throw new BadRequestException(Piwik::translate('General_ExceptionMethodNotFound', [$methodName, $className]));
547 }
548 }
549 /**
550 * @param $docComment
551 * @return bool
552 */
553 public function shouldHideAPIMethod($docComment)
554 {
555 $hideLine = strstr($docComment, '@hide');
556 if ($hideLine === \false) {
557 return \false;
558 }
559 $hideLine = trim($hideLine);
560 $hideLine .= ' ';
561 $token = trim(strtok($hideLine, " "), "\n");
562 $hide = \false;
563 if (!empty($token)) {
564 /**
565 * This event exists for checking whether a Plugin API class or a Plugin API method tagged
566 * with a `@hideXYZ` should be hidden in the API listing.
567 *
568 * @param bool &$hide whether to hide APIs tagged with $token should be displayed.
569 */
570 Piwik::postEvent(sprintf('API.DocumentationGenerator.%s', $token), array(&$hide));
571 }
572 return $hide;
573 }
574 /**
575 * @param ReflectionMethod $method
576 * @return bool
577 */
578 protected function checkIfMethodIsAvailable(ReflectionMethod $method)
579 {
580 if (!$method->isPublic() || $method->isConstructor() || $method->getName() === 'getInstance') {
581 return \false;
582 }
583 if ($this->hideIgnoredFunctions && \false !== strstr($method->getDocComment(), '@ignore')) {
584 return \false;
585 }
586 if ($this->shouldHideAPIMethod($method->getDocComment())) {
587 return \false;
588 }
589 return \true;
590 }
591 /**
592 * Returns true if the method is found in the API of the given class name.
593 *
594 * @param string $className The class name
595 * @param string $methodName The method name
596 * @return bool
597 */
598 private function isMethodAvailable($className, $methodName)
599 {
600 return isset($this->metadataArray[$className][$methodName]);
601 }
602 /**
603 * Checks that the class is a Singleton (presence of the getInstance() method)
604 *
605 * @param string $className The class name
606 * @throws Exception If the class is not a Singleton
607 */
608 private function checkClassIsSingleton($className)
609 {
610 if (!method_exists($className, "getInstance")) {
611 throw new Exception("{$className} that provide an API must be Singleton and have a 'public static function getInstance()' method.");
612 }
613 }
614 }
615