1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Core;
6:
7: /**
8: * This trait makes it possible for you to add dynamic methods
9: * into your object.
10: */
11: trait DynamicMethodTrait
12: {
13: /**
14: * Magic method - tries to call dynamic method and throws exception if
15: * this was not possible.
16: *
17: * @param string $name Name of the method
18: * @param array<int, mixed> $args Array of arguments to pass to this method
19: *
20: * @return mixed
21: */
22: public function __call(string $name, array $args)
23: {
24: $hookName = $this->buildMethodHookName($name);
25: if (TraitUtil::hasHookTrait($this) && $this->hookHasCallbacks($hookName)) {
26: $result = $this->hook($hookName, $args);
27:
28: return reset($result);
29: }
30:
31: // match native PHP behaviour as much as possible
32: // https://3v4l.org/eAv7t
33: $class = static::class;
34: do {
35: if (method_exists($class, $name)) {
36: $methodRefl = new \ReflectionMethod($class, $name);
37: $visibility = $methodRefl->isPrivate()
38: ? 'private'
39: : ($methodRefl->isProtected() ? 'protected' : 'unknown');
40: $fromScope = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? null;
41:
42: throw new \Error('Call to ' . $visibility . ' method ' . $class . '::' . $name . '() from '
43: . ($fromScope ? 'scope ' . $fromScope : 'global scope'));
44: }
45: } while ($class = get_parent_class($class));
46: $class = static::class;
47:
48: throw new \Error('Call to undefined method ' . $class . '::' . $name . '()');
49: }
50:
51: private function buildMethodHookName(string $name): string
52: {
53: return '__atk4__dynamic_method__' . $name;
54: }
55:
56: /**
57: * Add new method for this object.
58: *
59: * @param string $name Name of new method of $this object
60: *
61: * @return $this
62: */
63: public function addMethod(string $name, \Closure $fx)
64: {
65: if (!TraitUtil::hasHookTrait($this)) {
66: throw new Exception('Object must use HookTrait for dynamic method support');
67: }
68:
69: if ($this->hasMethod($name)) {
70: throw (new Exception('Method is already defined'))
71: ->addMoreInfo('name', $name);
72: }
73:
74: $this->onHook($this->buildMethodHookName($name), $fx);
75:
76: return $this;
77: }
78:
79: /**
80: * Return if this object has specified method (either native or dynamic).
81: *
82: * @param string $name Name of the method
83: */
84: public function hasMethod(string $name): bool
85: {
86: return method_exists($this, $name)
87: || $this->hookHasCallbacks($this->buildMethodHookName($name));
88: }
89:
90: /**
91: * Remove dynamically registered method.
92: *
93: * @param string $name Name of the method
94: *
95: * @return $this
96: */
97: public function removeMethod(string $name)
98: {
99: $this->removeHook($this->buildMethodHookName($name));
100:
101: return $this;
102: }
103: }
104: