Completed
Push — master ( a8fb97...ba7e2b )
by Mathieu
05:06
created

AbstractFactory::__construct()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 15
rs 8.8571
cc 5
eloc 9
nc 16
nop 1
1
<?php
2
3
namespace Charcoal\Factory;
4
5
// Dependencies from `PHP`
6
use \Exception;
7
use \InvalidArgumentException;
8
9
// Local namespace dependencies
10
use \Charcoal\Factory\FactoryInterface;
11
12
/**
13
 * Full implementation, as Abstract class, of the FactoryInterface.
14
 */
15
abstract class AbstractFactory implements FactoryInterface
16
{
17
    /**
18
     * @var array $resolved
19
     */
20
    static protected $resolved = [];
21
22
    /**
23
     * If a base class is set, then it must be ensured that the
24
     * @var string $baseClass
25
     */
26
    private $baseClass = '';
27
    /**
28
     *
29
     * @var string $defaultClass
30
     */
31
    private $defaultClass = '';
32
33
    /**
34
     * @var array $arguments
35
     */
36
    private $arguments;
37
38
    /**
39
     * @var callable $callback
40
     */
41
    private $callback;
42
43
    /**
44
     * Keeps loaded instances in memory, in `[$type => $instance]` format.
45
     * Used with the `get()` method only.
46
     * @var array $instances
47
     */
48
    private $instances = [];
49
50
    /**
51
     * @param array $data Constructor dependencies.
52
     */
53
    public function __construct(array $data=null)
54
    {
55
        if (isset($data['arguments'])) {
56
            $this->setArguments($data['arguments']);
57
        }
58
        if (isset($data['callback'])) {
59
            $this->setCallback($data['callback']);
60
        }
61
        if (isset($data['base_class'])) {
62
            $this->setBaseClass($data['base_class']);
63
        }
64
        if (isset($data['default_class'])) {
65
            $this->setDefaultClass($data['default_class']);
66
        }
67
    }
68
69
    /**
70
     * Create a new instance of a class, by type.
71
     *
72
     * Unlike `get()`, this method *always* return a new instance of the requested class.
73
     *
74
     * ## Object callback
75
     * It is possible to pass a callback method that will be executed upon object instanciation.
76
     * The callable should have a signature: `function($obj);` where $obj is the newly created object.
77
     *
78
     *
79
     * @param string   $type The type (class ident).
80
     * @param array    $args Optional. The constructor arguments. Leave blank to use `$arguments` member.
81
     * @param callable $cb   Optional. Object callback, called at creation. Leave blank to use `$callback` member.
82
     * @throws Exception If the base class is set and  the resulting instance is not of the base class.
83
     * @throws InvalidArgumentException If type argument is not a string or is not an available type.
84
     * @return mixed The instance / object
85
     */
86
    final public function create($type, array $args = null, callable $cb = null)
87
    {
88
        if (!is_string($type)) {
89
            throw new InvalidArgumentException(
90
                sprintf(
91
                    '%s: Type must be a string.',
92
                    get_called_class()
93
                )
94
            );
95
        }
96
97
        if (!isset($args)) {
98
            $args = $this->arguments();
99
        }
100
101
        if (!isset($cb)) {
102
            $cb = $this->callback();
103
        }
104
105
        $pool = get_called_class();
106
        if (isset(self::$resolved[$pool][$type])) {
107
            $classname = self::$resolved[$pool][$type];
108
        } else {
109
            if ($this->isResolvable($type) === false) {
110
                $defaultClass = $this->defaultClass();
111
                if ($defaultClass !== '') {
112
                    $obj = $this->createClass($defaultClass, $args);
0 ignored issues
show
Unused Code introduced by
$obj is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
113
                    $obj = new $defaultClass($args);
114
                    if (isset($cb)) {
115
                        $cb($obj);
116
                    }
117
                    return $obj;
118
                } else {
119
                    throw new InvalidArgumentException(
120
                        sprintf(
121
                            '%1$s: Type "%2$s" is not a valid type. (Using default class "%3$s")',
122
                            get_called_class(),
123
                            $type,
124
                            $defaultClass
125
                        )
126
                    );
127
                }
128
            }
129
130
            // Create the object from the type's class name.
131
            $classname = $this->resolve($type);
132
            self::$resolved[$pool][$type] = $classname;
133
        }
134
135
        $obj = $this->createClass($classname, $args);
136
137
        // Ensure base class is respected, if set.
138
        $baseClass = $this->baseClass();
139
        if ($baseClass !== '' && !($obj instanceof $baseClass)) {
140
            throw new Exception(
141
                sprintf(
142
                    '%1$s: Object is not a valid "%2$s" class',
143
                    get_called_class(),
144
                    $baseClass
145
                )
146
            );
147
        }
148
149
        if (isset($cb)) {
150
            $cb($obj);
151
        }
152
153
        return $obj;
154
    }
155
156
    /**
157
     * Create a class instance with given arguments.
158
     *
159
     * How the constructor arguments are passed depends on its type:
160
     *
161
     * - if null, no arguments are passed at all.
162
     * - if it's not an array, it's passed as a single argument.
163
     * - if it's an associative array, it's passed as a sing argument.
164
     * - if it's a sequential (numeric keys) array, it's
165
     */
166
    public function createClass($classname, $args)
167
    {
168
        if ($args === null) {
169
            return new $classname;
170
        }
171
        if (!is_array($args)) {
172
            return new $classname($args);
173
        }
174
        if (count(array_filter(array_keys($args), 'is_string')) > 0) {
175
176
            return new $classname($args);
177
        } else {
178
            if (version_compare(PHP_VERSION, '5.6.0') >= 0) {
179
                $obj = new $classname(...$args);
0 ignored issues
show
Unused Code introduced by
$obj is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
180
            } else {
181
                // PHP under 5.6 does not support argument unpacking.
182
                $reflection = new \ReflectionClass($classname);
183
                $obj = $reflection->newInstanceArgs($args);
0 ignored issues
show
Unused Code introduced by
$obj is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
184
            }
185
        }
186
    }
187
188
    /**
189
     * Get (load or create) an instance of a class, by type.
190
     *
191
     * Unlike `create()` (which always call a `new` instance), this function first tries to load / reuse
192
     * an already created object of this type, from memory.
193
     *
194
     * @param string $type The type (class ident).
195
     * @param array  $args The constructor arguments (optional).
196
     * @throws InvalidArgumentException If type argument is not a string.
197
     * @return mixed The instance / object
198
     */
199
    final public function get($type, array $args = null)
200
    {
201
        if (!is_string($type)) {
202
            throw new InvalidArgumentException(
203
                'Type must be a string.'
204
            );
205
        }
206
        if (!isset($this->instances[$type]) || $this->instances[$type] === null) {
207
            $this->instances[$type] = $this->create($type, $args);
208
        }
209
        return $this->instances[$type];
210
    }
211
212
    /**
213
     * If a base class is set, then it must be ensured that the created objects
214
     * are `instanceof` this base class.
215
     *
216
     * @param string $type The FQN of the class, or "type" of object, to set as base class.
217
     * @throws InvalidArgumentException If the class is not a string or is not an existing class / interface.
218
     * @return FactoryInterface
219
     */
220
    public function setBaseClass($type)
221
    {
222
        if (!is_string($type) || empty($type)) {
223
            throw new InvalidArgumentException(
224
                'Class name or type must be a non-empty string.'
225
            );
226
        }
227
228
        $exists = (class_exists($type) || interface_exists($type));
229
        if ($exists) {
230
            $classname = $type;
231 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
232
            $classname = $this->resolve($type);
233
234
            $exists = (class_exists($classname) || interface_exists($classname));
235
            if (!$exists) {
236
                throw new InvalidArgumentException(
237
                    sprintf('Can not set "%s" as base class: Invalid class or interface name.', $classname)
238
                );
239
            }
240
        }
241
242
        $this->baseClass = $classname;
243
244
        return $this;
245
    }
246
247
    /**
248
     * @return string The FQN of the base class
249
     */
250
    public function baseClass()
251
    {
252
        return $this->baseClass;
253
    }
254
255
    /**
256
     * If a default class is set, then calling `get()` or `create()` an invalid type
257
     * should return an object of this class instead of throwing an error.
258
     *
259
     * @param string $type The FQN of the class, or "type" of object, to set as default class.
260
     * @throws InvalidArgumentException If the class name is not a string or not a valid class.
261
     * @return FactoryInterface
262
     */
263
    public function setDefaultClass($type)
264
    {
265
        if (!is_string($type) || empty($type)) {
266
            throw new InvalidArgumentException(
267
                'Class name or type must be a non-empty string.'
268
            );
269
        }
270
271 View Code Duplication
        if (class_exists($type)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
272
            $classname = $type;
273
        } else {
274
            $classname = $this->resolve($type);
275
276
            if (!class_exists($classname)) {
277
                throw new InvalidArgumentException(
278
                    sprintf('Can not set "%s" as defaut class: Invalid class name.', $classname)
279
                );
280
            }
281
        }
282
283
        $this->defaultClass = $classname;
284
285
        return $this;
286
    }
287
288
    /**
289
     * @return string The FQN of the default class
290
     */
291
    public function defaultClass()
292
    {
293
        return $this->defaultClass;
294
    }
295
296
    /**
297
     * @param array $arguments The constructor arguments to be passed to the created object's initialization.
298
     * @return AbstractFactory Chainable
299
     */
300
    public function setArguments(array $arguments)
301
    {
302
        $this->arguments = $arguments;
303
        return $this;
304
    }
305
306
    /**
307
     * @return array
308
     */
309
    public function arguments()
310
    {
311
        return $this->arguments;
312
    }
313
314
    /**
315
     * @param callable $callback The object callback.
316
     * @return AbstractFatory Chainable
317
     */
318
    public function setCallback(callable $callback)
319
    {
320
        $this->callback = $callback;
321
        return $this;
322
    }
323
324
    /**
325
     * @return callable|null
326
     */
327
    public function callback()
328
    {
329
        return $this->callback;
330
    }
331
332
    /**
333
     * Resolve the class name from "type".
334
     *
335
     * @param string $type The "type" of object to resolve (the object ident).
336
     * @return string The resolved class name (FQN).
337
     */
338
    abstract public function resolve($type);
339
340
    /**
341
     * Returns wether a type is resolvable (is valid).
342
     *
343
     * @param string $type The "type" of object to resolve (the object ident).
344
     * @return boolean True if the type is available, false if not
345
     */
346
    abstract public function isResolvable($type);
347
}
348