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