Completed
Push — master ( 6413d9...47c4ae )
by Mathieu
03:10
created

AbstractFactory::setCallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
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
     * If a base class is set, then it must be ensured that the
19
     * @var string $baseClass
20
     */
21
    private $baseClass = '';
22
    /**
23
     *
24
     * @var string $defaultClass
25
     */
26
    private $defaultClass = '';
27
28
    /**
29
     * @var array $arguments
30
     */
31
    private $arguments = null;
32
33
    /**
34
     * @var callable $callback
35
     */
36
    private $callback = null;
37
38
    /**
39
     * Keeps loaded instances in memory, in `[$type => $instance]` format.
40
     * Used with the `get()` method only.
41
     * @var array $instances
42
     */
43
    private $instances = [];
44
45
    /**
46
     * Create a new instance of a class, by type.
47
     *
48
     * Unlike `get()`, this method *always* return a new instance of the requested class.
49
     *
50
     * ## Object callback
51
     * It is possible to pass a callback method that will be executed upon object instanciation.
52
     * The callable should have a signature: `function($obj);` where $obj is the newly created object.
53
     *
54
     *
55
     * @param string   $type The type (class ident).
56
     * @param array    $args Optional. The constructor arguments. Leave blank to use `$arguments` member.
57
     * @param callable $cb   Optional. Object callback, called at creation. Leave blank to use `$callback` member.
58
     * @throws Exception If the base class is set and  the resulting instance is not of the base class.
59
     * @throws InvalidArgumentException If type argument is not a string or is not an available type.
60
     * @return mixed The instance / object
61
     */
62
    final public function create($type, array $args = null, callable $cb = null)
63
    {
64
        if (!is_string($type)) {
65
            throw new InvalidArgumentException(
66
                sprintf(
67
                    '%s: Type must be a string.',
68
                    get_called_class()
69
                )
70
            );
71
        }
72
73
        if (!isset($args)) {
74
            $args = $this->arguments();
75
        }
76
77
        if (!isset($cb)) {
78
            $cb = $this->callback();
79
        }
80
81
        if ($this->isResolvable($type) === false) {
82
            $defaultClass = $this->defaultClass();
83
            if ($defaultClass !== '') {
84
                $obj = new $defaultClass($args);
85
                if (isset($cb)) {
86
                    $cb($obj);
87
                }
88
                return $obj;
89
            } else {
90
                throw new InvalidArgumentException(
91
                    sprintf(
92
                        '%1$s: Type "%2$s" is not a valid type. (Using default class "%3$s")',
93
                        get_called_class(),
94
                        $type,
95
                        $defaultClass
96
                    )
97
                );
98
            }
99
        }
100
101
        // Create the object from the type's class name.
102
        $classname = $this->resolve($type);
103
        $obj = new $classname($args);
104
105
106
        // Ensure base class is respected, if set.
107
        $baseClass = $this->baseClass();
108
        if ($baseClass !== '' && !($obj instanceof $baseClass)) {
109
            throw new Exception(
110
                sprintf(
111
                    '%1$s: Object is not a valid "%2$s" class',
112
                    get_called_class(),
113
                    $baseClass
114
                )
115
            );
116
        }
117
118
        if (isset($cb)) {
119
            $cb($obj);
120
        }
121
122
        return $obj;
123
    }
124
125
    /**
126
     * Get (load or create) an instance of a class, by type.
127
     *
128
     * Unlike `create()` (which always call a `new` instance), this function first tries to load / reuse
129
     * an already created object of this type, from memory.
130
     *
131
     * @param string $type The type (class ident).
132
     * @param array  $args The constructor arguments (optional).
133
     * @throws InvalidArgumentException If type argument is not a string.
134
     * @return mixed The instance / object
135
     */
136
    final public function get($type, array $args = null)
137
    {
138
        if (!is_string($type)) {
139
            throw new InvalidArgumentException(
140
                'Type must be a string.'
141
            );
142
        }
143
        if (!isset($this->instances[$type]) || $this->instances[$type] === null) {
144
            $this->instances[$type] = $this->create($type, $args);
145
        }
146
        return $this->instances[$type];
147
    }
148
149
    /**
150
     * If a base class is set, then it must be ensured that the created objects
151
     * are `instanceof` this base class.
152
     *
153
     * @param string $type The FQN of the class, or "type" of object, to set as base class.
154
     * @throws InvalidArgumentException If the class is not a string or is not an existing class / interface.
155
     * @return FactoryInterface
156
     */
157
    public function setBaseClass($type)
158
    {
159
        if (!is_string($type) || empty($type)) {
160
            throw new InvalidArgumentException(
161
                'Class name or type must be a non-empty string.'
162
            );
163
        }
164
165
        $exists = (class_exists($type) || interface_exists($type));
166
        if ($exists) {
167
            $classname = $type;
168 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...
169
            $classname = $this->resolve($type);
170
171
            $exists = (class_exists($classname) || interface_exists($classname));
172
            if (!$exists) {
173
                throw new InvalidArgumentException(
174
                    sprintf('Can not set "%s" as base class: Invalid class or interface name.', $classname)
175
                );
176
            }
177
        }
178
179
        $this->baseClass = $classname;
180
181
        return $this;
182
    }
183
184
    /**
185
     * @return string The FQN of the base class
186
     */
187
    public function baseClass()
188
    {
189
        return $this->baseClass;
190
    }
191
192
    /**
193
     * If a default class is set, then calling `get()` or `create()` an invalid type
194
     * should return an object of this class instead of throwing an error.
195
     *
196
     * @param string $type The FQN of the class, or "type" of object, to set as default class.
197
     * @throws InvalidArgumentException If the class name is not a string or not a valid class.
198
     * @return FactoryInterface
199
     */
200
    public function setDefaultClass($type)
201
    {
202
        if (!is_string($type) || empty($type)) {
203
            throw new InvalidArgumentException(
204
                'Class name or type must be a non-empty string.'
205
            );
206
        }
207
208 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...
209
            $classname = $type;
210
        } else {
211
            $classname = $this->resolve($type);
212
213
            if (!class_exists($classname)) {
214
                throw new InvalidArgumentException(
215
                    sprintf('Can not set "%s" as defaut class: Invalid class name.', $classname)
216
                );
217
            }
218
        }
219
220
        $this->defaultClass = $classname;
221
222
        return $this;
223
    }
224
225
    /**
226
     * @return string The FQN of the default class
227
     */
228
    public function defaultClass()
229
    {
230
        return $this->defaultClass;
231
    }
232
233
    /**
234
     * @param array $arguments The constructor arguments to be passed to the created object's initialization.
235
     * @return AbstractFactory Chainable
236
     */
237
    public function setArguments(array $arguments)
238
    {
239
        $this->arguments = $arguments;
240
        return $this;
241
    }
242
243
    /**
244
     * @return array
245
     */
246
    public function arguments()
247
    {
248
        return $this->arguments;
249
    }
250
251
    /**
252
     * @param callable $callback The object callback.
253
     * @return AbstractFatory Chainable
254
     */
255
    public function setCallback(callable $callback)
256
    {
257
        $this->callback = $callback;
258
        return $this;
259
    }
260
261
    /**
262
     * @return callable|null
263
     */
264
    public function callback()
265
    {
266
        return $this->callback;
267
    }
268
269
    /**
270
     * Resolve the class name from "type".
271
     *
272
     * @param string $type The "type" of object to resolve (the object ident).
273
     * @return string
274
     */
275
    abstract public function resolve($type);
276
277
    /**
278
     * Returns wether a type is resolvable (is valid).
279
     *
280
     * @param string $type The "type" of object to resolve (the object ident).
281
     * @return boolean True if the type is available, false if not
282
     */
283
    abstract public function isResolvable($type);
284
}
285