Completed
Push — master ( 65bf09...6413d9 )
by Mathieu
27:27
created

AbstractFactory::create()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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