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