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