blob: b9c868dff5004ee35a58fc2c4ba5e516397a241b [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/**
4 * This file is part of the Carbon package.
5 *
6 * (c) Brian Nesbitt <brian@nesbot.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11namespace Carbon\Traits;
12
13use Closure;
14use Generator;
15use ReflectionClass;
16use ReflectionException;
17use ReflectionMethod;
18use Throwable;
19
20/**
21 * Trait Mixin.
22 *
23 * Allows mixing in entire classes with multiple macros.
24 */
25trait Mixin
26{
27 /**
28 * Stack of macro instance contexts.
29 *
30 * @var array
31 */
32 protected static $macroContextStack = [];
33
34 /**
35 * Mix another object into the class.
36 *
37 * @example
38 * ```
39 * Carbon::mixin(new class {
40 * public function addMoon() {
41 * return function () {
42 * return $this->addDays(30);
43 * };
44 * }
45 * public function subMoon() {
46 * return function () {
47 * return $this->subDays(30);
48 * };
49 * }
50 * });
51 * $fullMoon = Carbon::create('2018-12-22');
52 * $nextFullMoon = $fullMoon->addMoon();
53 * $blackMoon = Carbon::create('2019-01-06');
54 * $previousBlackMoon = $blackMoon->subMoon();
55 * echo "$nextFullMoon\n";
56 * echo "$previousBlackMoon\n";
57 * ```
58 *
59 * @param object|string $mixin
60 *
61 * @throws ReflectionException
62 *
63 * @return void
64 */
65 public static function mixin($mixin)
66 {
67 \is_string($mixin) && trait_exists($mixin)
68 ? static::loadMixinTrait($mixin)
69 : static::loadMixinClass($mixin);
70 }
71
72 /**
73 * @param object|string $mixin
74 *
75 * @throws ReflectionException
76 */
77 private static function loadMixinClass($mixin)
78 {
79 $methods = (new ReflectionClass($mixin))->getMethods(
80 ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
81 );
82
83 foreach ($methods as $method) {
84 if ($method->isConstructor() || $method->isDestructor()) {
85 continue;
86 }
87
88 $method->setAccessible(true);
89
90 static::macro($method->name, $method->invoke($mixin));
91 }
92 }
93
94 /**
95 * @param string $trait
96 */
97 private static function loadMixinTrait($trait)
98 {
99 $context = eval(self::getAnonymousClassCodeForTrait($trait));
100 $className = \get_class($context);
101
102 foreach (self::getMixableMethods($context) as $name) {
103 $closureBase = Closure::fromCallable([$context, $name]);
104
105 static::macro($name, function () use ($closureBase, $className) {
106 /** @phpstan-ignore-next-line */
107 $context = isset($this) ? $this->cast($className) : new $className();
108
109 try {
110 // @ is required to handle error if not converted into exceptions
111 $closure = @$closureBase->bindTo($context);
112 } catch (Throwable $throwable) { // @codeCoverageIgnore
113 $closure = $closureBase; // @codeCoverageIgnore
114 }
115
116 // in case of errors not converted into exceptions
117 $closure = $closure ?? $closureBase;
118
119 return $closure(...\func_get_args());
120 });
121 }
122 }
123
124 private static function getAnonymousClassCodeForTrait(string $trait)
125 {
126 return 'return new class() extends '.static::class.' {use '.$trait.';};';
127 }
128
129 private static function getMixableMethods(self $context): Generator
130 {
131 foreach (get_class_methods($context) as $name) {
132 if (method_exists(static::class, $name)) {
133 continue;
134 }
135
136 yield $name;
137 }
138 }
139
140 /**
141 * Stack a Carbon context from inside calls of self::this() and execute a given action.
142 *
143 * @param static|null $context
144 * @param callable $callable
145 *
146 * @throws Throwable
147 *
148 * @return mixed
149 */
150 protected static function bindMacroContext($context, callable $callable)
151 {
152 static::$macroContextStack[] = $context;
153 $exception = null;
154 $result = null;
155
156 try {
157 $result = $callable();
158 } catch (Throwable $throwable) {
159 $exception = $throwable;
160 }
161
162 array_pop(static::$macroContextStack);
163
164 if ($exception) {
165 throw $exception;
166 }
167
168 return $result;
169 }
170
171 /**
172 * Return the current context from inside a macro callee or a null if static.
173 *
174 * @return static|null
175 */
176 protected static function context()
177 {
178 return end(static::$macroContextStack) ?: null;
179 }
180
181 /**
182 * Return the current context from inside a macro callee or a new one if static.
183 *
184 * @return static
185 */
186 protected static function this()
187 {
188 return end(static::$macroContextStack) ?: new static();
189 }
190}