Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.00% covered (success)
96.00%
48 / 50
90.91% covered (success)
90.91%
10 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Micro
96.00% covered (success)
96.00%
48 / 50
90.91% covered (success)
90.91%
10 / 11
29
0.00% covered (danger)
0.00%
0 / 1
 run
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 app
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasInterface
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClass
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 interfaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
5.09
 createDependencies
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 getCallable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isMicroCallable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace Dynart\Micro;
4
5use ReflectionClass;
6use ReflectionException;
7
8/**
9 * Micro PHP Dependency Injection
10 */
11class Micro {
12
13    /** Holds the instance of the application */
14    protected static ?AbstractApp $app = null;
15
16    /** Stores the classes in [interface => class] format, the class can be null */
17    protected static array $classes = [];
18
19    /** Stores the instances in [interface => instance] format */
20    protected static array $instances = [];
21
22    /**
23     * Sets the application instance and runs it
24     *
25     * First it sets the instance, then calls the `fullInit()` and `fullProcess()` methods of the `$app`.
26     *
27     * @throws MicroException if the instance was set before
28     */
29    public static function run(AbstractApp $app): void {
30        if (self::$app) {
31            throw new MicroException("App was instantiated before!");
32        }
33        self::$app = $app;
34        $app->fullInit();
35        $app->fullProcess();
36    }
37
38    /**
39     * Returns the instance of the application
40     */
41    public static function app(): ?AbstractApp {
42        return self::$app;
43    }
44
45    /**
46     * Adds a class for an interface
47     *
48     * For example:
49     *
50     * <pre>
51     * Micro::add(ConfigInterface::class, Config::class);
52     * </pre>
53     *
54     * or
55     *
56     * <pre>
57     * Micro::add(Config::class);
58     * </pre>
59     */
60    public static function add(string $interface, ?string $class = null): void {
61        if ($class != null && !(is_subclass_of($class, $interface))) {
62            throw new MicroException("$class does not implement $interface");
63        }
64        self::$classes[$interface] = $class;
65    }
66
67    public static function hasInterface(string $interface): bool {
68        return array_key_exists($interface, self::$classes);
69    }
70
71    /**
72     * Returns with the class for the given interface
73     *
74     * @throws MicroException If the interface wasn't added
75     */
76    public static function getClass(string $interface): string {
77        if (!self::hasInterface($interface)) {
78            throw new MicroException("$interface was not added");
79        }
80        return self::$classes[$interface] ?? $interface;
81    }
82
83    /**
84     * Creates the singleton instance for the given interface, stores it in `$instances`, then returns with it
85     *
86     * It returns instantly if the instance was stored before.
87     *
88     * @throws MicroException
89     */
90    public static function get(string $interface, array $parameters = [], array $dependencyStack = []): object {
91        if (array_key_exists($interface, self::$instances)) {
92            return self::$instances[$interface];
93        }
94        $result = self::create(self::getClass($interface), $parameters, $dependencyStack);
95        self::$instances[$interface] = $result;
96        return $result;
97    }
98
99    /**
100     * Returns with all the interfaces in an array
101     */
102    public static function interfaces(): array {
103        return array_keys(self::$classes);
104    }
105
106    /**
107     * Creates an instance for the given class
108     *
109     * In the following example, the `Something` class constructor will get the `Config` instance
110     * and the 'someParameterValue' in the `$someParameter`.
111     *
112     * <pre>
113     * use Dynart\Micro\Micro;
114     * use Dynart\Micro\App;
115     * use Dynart\Micro\Config;
116     *
117     * class Something {
118     *   private $someParameter;
119     *   public function __construct(Config $config, $someParameter) {
120     *     $this->someParameter = $someParameter;
121     *   }
122     *
123     *   public function someParameter() {
124     *     return $this->someParameter;
125     *   }
126     * }
127     *
128     * class App extends AbstractApp {
129     *   private $something;
130     *   public function __construct() {
131     *     Micro::add(Config::class);
132     *     Micro::add(Something::class);
133     *   }
134     *
135     *   public function init() {
136     *     $this->something = Micro::create(Something::class, ['someParameterValue']);
137     *   }
138     *
139     *   public function process() {
140     *     echo $this->something->someParameter();
141     *   }
142     * }
143     * </pre>
144     *
145     * If the class has a `postConstruct()` method it will be called after creation. It can be used for lazy injection.
146     *
147     * @throws MicroException
148     */
149    public static function create(string $class, array $parameters = [], array $dependencyStack = []): object {
150        if (in_array($class, $dependencyStack)) {
151            throw new MicroException("Circular dependency: ".join(" <- ", $dependencyStack));
152        }
153        $dependencyStack[] = $class;
154        try {
155            $reflectionClass = new ReflectionClass($class);
156        } catch (ReflectionException $e) {
157            throw new MicroException("Couldn't create reflection class for `$class`");
158        }
159        $dependencies = self::createDependencies($class, $reflectionClass, $dependencyStack);
160        try {
161            $result = $reflectionClass->newInstanceArgs(array_merge($dependencies, $parameters));
162        } catch (ReflectionException $e) {
163            throw new MicroException("Couldn't create class `$class`:\nMessage: ".$e->getMessage()."\n".$e->getTraceAsString());
164        }
165        if (method_exists($result, 'postConstruct')) {
166            $result->postConstruct();
167        }
168        return $result;
169    }
170
171    /**
172     * Creates the singleton dependencies for a given class and returns with it as an array
173     *
174     * @throws MicroException
175     */
176    private static function createDependencies(string $class, ReflectionClass $reflectionClass, array $dependencyStack = []): array {
177        $result = [];
178        $constructor = $reflectionClass->getConstructor();
179        if (!$constructor) {
180            return $result;
181        }
182        foreach ($constructor->getParameters() as $parameter) {
183            $type = $parameter->getType();
184            if (!$type || $type->isBuiltin()) {
185                continue;
186            }
187            $interface = $type->getName();
188            if (self::hasInterface($interface)) {
189                $result[] = self::get($interface, [], $dependencyStack);
190            } else {
191                throw new MicroException("Non existing dependency `$interface` for `$class`");
192            }
193        }
194        return $result;
195    }
196
197    /**
198     * Creates an instance of the callable if needed, then returns with it
199     *
200     * @throws MicroException
201     */
202    public static function getCallable(mixed $callable): mixed {
203        return self::isMicroCallable($callable) ? [Micro::get($callable[0]), $callable[1]] : $callable;
204    }
205
206    /**
207     * Returns true if the `$callable` is a Micro Framework callable
208     *
209     * Micro Framework callable means: an array with two strings.
210     * The first one is the class name, the second is the method name.
211     *
212     * Example:
213     * <pre>
214     * [Something::class, 'theMethodName']
215     * </pre>
216     */
217    public static function isMicroCallable(mixed $callable): bool {
218        return is_array($callable)
219            && count($callable) == 2
220            && is_string($callable[0])
221            && is_string($callable[1]);
222    }
223}