briannesbitt / Carbon
1
<?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
 */
11
namespace Carbon\Traits;
12

13
use Closure;
14
use Generator;
15
use ReflectionClass;
16
use ReflectionException;
17
use ReflectionMethod;
18
use Throwable;
19

20
/**
21
 * Trait Mixin.
22
 *
23
 * Allows mixing in entire classes with multiple macros.
24
 */
25
trait 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 2
    public static function mixin($mixin)
66
    {
67 2
        \is_string($mixin) && trait_exists($mixin)
68 2
            ? static::loadMixinTrait($mixin)
69 2
            : static::loadMixinClass($mixin);
70
    }
71

72
    /**
73
     * @param object|string $mixin
74
     *
75
     * @throws ReflectionException
76
     */
77 2
    private static function loadMixinClass($mixin)
78
    {
79 2
        $methods = (new ReflectionClass($mixin))->getMethods(
80 2
            ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
81
        );
82

83 2
        foreach ($methods as $method) {
84 2
            if ($method->isConstructor() || $method->isDestructor()) {
85 2
                continue;
86
            }
87

88 2
            $method->setAccessible(true);
89

90 2
            static::macro($method->name, $method->invoke($mixin));
91
        }
92
    }
93

94
    /**
95
     * @param string $trait
96
     */
97 2
    private static function loadMixinTrait($trait)
98
    {
99 2
        $context = eval(self::getAnonymousClassCodeForTrait($trait));
100 2
        $className = \get_class($context);
101

102 2
        foreach (self::getMixableMethods($context) as $name) {
103 2
            $closureBase = Closure::fromCallable([$context, $name]);
104

105
            static::macro($name, function () use ($closureBase, $className) {
106
                /** @phpstan-ignore-next-line */
107 2
                $context = isset($this) ? $this->cast($className) : new $className();
108

109
                try {
110
                    // @ is required to handle error if not converted into exceptions
111 2
                    $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 2
                $closure = $closure ?? $closureBase;
118

119 2
                return $closure(...\func_get_args());
120 2
            });
121
        }
122
    }
123

124 2
    private static function getAnonymousClassCodeForTrait(string $trait)
125
    {
126 2
        return 'return new class() extends '.static::class.' {use '.$trait.';};';
127
    }
128

129 2
    private static function getMixableMethods(self $context): Generator
130
    {
131 2
        foreach (get_class_methods($context) as $name) {
132 2
            if (method_exists(static::class, $name)) {
133 2
                continue;
134
            }
135

136 2
            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 2
    protected static function bindMacroContext($context, callable $callable)
151
    {
152 2
        static::$macroContextStack[] = $context;
153 2
        $exception = null;
154 2
        $result = null;
155

156
        try {
157 2
            $result = $callable();
158 2
        } catch (Throwable $throwable) {
159 2
            $exception = $throwable;
160
        }
161

162 2
        array_pop(static::$macroContextStack);
163

164 2
        if ($exception) {
165 2
            throw $exception;
166
        }
167

168 2
        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 2
    protected static function context()
177
    {
178 2
        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 2
    protected static function this()
187
    {
188 2
        return end(static::$macroContextStack) ?: new static();
189
    }
190
}

Read our documentation on viewing source code .

Loading