Completed
Push — master ( a4db1b...f41d1c )
by
unknown
02:57 queued 28s
created

ServiceFactory::manageServiceData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/*
3
 * 2016 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 Configuration Object project.
6
 * It is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License, either
8
 * version 3 of the License, or any later version.
9
 *
10
 * For the full copyright and license information, see:
11
 * http://www.gnu.org/licenses/gpl-3.0.html
12
 */
13
14
namespace Romm\ConfigurationObject\Service;
15
16
use Romm\ConfigurationObject\Core\Core;
17
use Romm\ConfigurationObject\Exceptions\DuplicateEntryException;
18
use Romm\ConfigurationObject\Exceptions\EntryNotFoundException;
19
use Romm\ConfigurationObject\Exceptions\Exception;
20
use Romm\ConfigurationObject\Exceptions\InitializationNotSetException;
21
use Romm\ConfigurationObject\Exceptions\InvalidTypeException;
22
use Romm\ConfigurationObject\Exceptions\MethodNotFoundException;
23
use Romm\ConfigurationObject\Service\DataTransferObject\AbstractServiceDTO;
24
use Romm\ConfigurationObject\Exceptions\WrongInheritanceException;
25
use Romm\ConfigurationObject\Service\Event\ServiceEventInterface;
26
use Romm\ConfigurationObject\Traits\InternalVariablesTrait;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Extbase\Reflection\ClassReflection;
29
30
/**
31
 * This class will handle the several services which will be used for a
32
 * configuration object.
33
 *
34
 * You may customize which services are used by overriding the function
35
 * `getConfigurationObjectServices` in your configuration object root class.
36
 *
37
 * Example:
38
 *
39
 *  public static function getConfigurationObjectServices()
40
 *  {
41
 *      return ServiceFactory::getInstance()
42
 *          ->attach(CacheService::class)
43
 *          ->setOption(CacheService::OPTION_CACHE_NAME, 'my_custom_cache')
44
 *          ->attach(MyCustomService::class, ['foo' => 'bar'])
45
 *          ->setOption('optionForMyCustomService', 'foo');
46
 *  }
47
 */
48
class ServiceFactory
49
{
50
51
    use InternalVariablesTrait;
52
53
    /**
54
     * @var array
55
     */
56
    protected $service = [];
57
58
    /**
59
     * @var AbstractService[]
60
     */
61
    protected $serviceInstances = [];
62
63
    /**
64
     * @var bool
65
     */
66
    protected $hasBeenInitialized = false;
67
68
    /**
69
     * @var array
70
     */
71
    protected static $instances = [];
72
73
    /**
74
     * @var array
75
     */
76
    protected $servicesEvents = [];
77
78
    /**
79
     * @var ClassReflection[]
80
     */
81
    protected $serviceClassReflection = [];
82
83
    /**
84
     * @var string
85
     */
86
    private $currentService;
87
88
    /**
89
     * @var array
90
     */
91
    protected static $servicesChecked = [];
92
93
    /**
94
     * Main function to get an empty instance of ServiceFactory.
95
     *
96
     * @return ServiceFactory
97
     */
98
    public static function getInstance()
99
    {
100
        return Core::get()->getServiceFactoryInstance();
101
    }
102
103
    /**
104
     * Will attach a service to this factory.
105
     *
106
     * It will also reset the current service value to the given service,
107
     * allowing the usage of the function `setOption` with this service.
108
     *
109
     * @param string $serviceClassName The class name of the service which will be attached.
110
     * @param array  $options          Array of options which will be sent to the service.
111
     * @return $this
112
     * @throws DuplicateEntryException
113
     */
114
    public function attach($serviceClassName, array $options = [])
115
    {
116
        if (true === isset($this->service[$serviceClassName])) {
117
            throw new DuplicateEntryException(
118
                'The service "' . $serviceClassName . '" was already attached to the service factory.',
119
                1456418859
120
            );
121
        }
122
123
        $this->currentService = $serviceClassName;
124
        $this->service[$serviceClassName] = [
125
            'className' => $serviceClassName,
126
            'options'   => $options
127
        ];
128
129
        return $this;
130
    }
131
132
    /**
133
     * Checks if this factory contains the given service.
134
     *
135
     * @param string $serviceClassName The class name of the service which should be attached.
136
     * @return bool
137
     */
138
    public function has($serviceClassName)
139
    {
140
        return (true === isset($this->service[$serviceClassName]));
141
    }
142
143
    /**
144
     * Returns the wanted service, if it was previously registered.
145
     *
146
     * @param string $serviceClassName The class name of the wanted service.
147
     * @return AbstractService
148
     * @throws InitializationNotSetException
149
     * @throws EntryNotFoundException
150
     */
151
    public function get($serviceClassName)
152
    {
153
        if (false === $this->hasBeenInitialized) {
154
            throw new InitializationNotSetException(
155
                'You can get a service instance only when the service factory has been initialized.',
156
                1456419587
157
            );
158
        }
159
160
        if (false === $this->has($serviceClassName)) {
161
            throw new EntryNotFoundException(
162
                'The service "' . $serviceClassName . '" was not found in this service factory. Attach it before trying to get it!',
163
                1456419653
164
            );
165
        }
166
167
        return $this->serviceInstances[$serviceClassName];
168
    }
169
170
    /**
171
     * Resets the current service value to the given service, allowing the usage
172
     * of the function `setOption` with this service.
173
     *
174
     * @param string $serviceClassName The class name of the service which will be saved as "current service".
175
     * @return $this
176
     * @throws Exception
177
     */
178
    public function with($serviceClassName)
179
    {
180
        if (false === $this->has($serviceClassName)) {
181
            throw new Exception(
182
                'You cannot use the function "' . __FUNCTION__ . '" on a service which was not added to the factory (service used: "' . $serviceClassName . '").',
183
                1459425398
184
            );
185
        }
186
187
        $this->currentService = $serviceClassName;
188
189
        return $this;
190
    }
191
192
    /**
193
     * Allows the modification of a given option for the current service (which
194
     * was defined in the function `with()` or the function `attach()`).
195
     *
196
     * @param string $optionName  Name of the option to modify.
197
     * @param string $optionValue New value of the option.
198
     * @return $this
199
     * @throws InitializationNotSetException
200
     */
201
    public function setOption($optionName, $optionValue)
202
    {
203
        if (null === $this->currentService) {
204
            throw new InitializationNotSetException(
205
                'To set the option "' . (string)$optionName . '" you need to indicate a service first, by using the function "with".',
206
                1456419282
207
            );
208
        }
209
210
        $this->service[$this->currentService]['options'][$optionName] = $optionValue;
211
212
        return $this;
213
    }
214
215
    /**
216
     * Returns an option for the current service. If the option is not found,
217
     * `null` is returned.
218
     *
219
     * @param string $optionName
220
     * @return mixed
221
     */
222
    public function getOption($optionName)
223
    {
224
        return (isset($this->service[$this->currentService]['options'][$optionName]))
225
            ? $this->service[$this->currentService]['options'][$optionName]
226
            : null;
227
    }
228
229
    /**
230
     * Initializes every single service which was added in this instance.
231
     *
232
     * @throws WrongInheritanceException
233
     * @internal This function is reserved for internal usage only, you should not use it in third party applications!
234
     */
235
    public function initialize()
236
    {
237
        if (true === $this->hasBeenInitialized) {
238
            return;
239
        }
240
241
        $this->hasBeenInitialized = true;
242
243
        foreach ($this->service as $service) {
244
            list($serviceClassName,  $serviceOptions) = $this->manageServiceData($service);
245
246
            $this->serviceInstances[$serviceClassName] = $this->getServiceInstance($serviceClassName, $serviceOptions);
247
        }
248
    }
249
250
    /**
251
     * @param string $className
252
     * @param array  $options
253
     * @return AbstractService
254
     * @throws WrongInheritanceException
255
     */
256
    protected function getServiceInstance($className, array $options)
257
    {
258
        $flag = false;
259
        $serviceInstance = null;
260
261
        if (Core::get()->classExists($className)) {
262
            /** @var AbstractService $serviceInstance */
263
            $serviceInstance = Core::get()->getObjectManager()->get($className);
264
265
            if ($serviceInstance instanceof AbstractService) {
266
                $serviceInstance->initializeObject($options);
267
                $serviceInstance->initialize();
268
                $flag = true;
269
            }
270
        }
271
272
        if (false === $flag) {
273
            throw new WrongInheritanceException(
274
                'Trying to initialize ConfigurationObject with a wrong service: "' . $className . '".',
275
                1448886469
276
            );
277
        }
278
279
        return $serviceInstance;
280
    }
281
282
    /**
283
     * Will loop on each registered service in this factory, and check if they
284
     * use the requested event. If they do, the event is dispatched.
285
     *
286
     * @param string             $serviceEvent    The class name of the service event.
287
     * @param string             $eventMethodName Name of the method called in the service.
288
     * @param AbstractServiceDTO $dto             The data transfer object sent to the services.
289
     * @throws Exception
290
     * @throws InvalidTypeException
291
     * @throws WrongInheritanceException
292
     * @internal This function is reserved for internal usage only, you should not use it in third party applications!
293
     */
294
    public function runServicesFromEvent($serviceEvent, $eventMethodName, AbstractServiceDTO $dto)
295
    {
296
        if (false === $this->hasBeenInitialized) {
297
            return;
298
        }
299
300
        $this->checkServiceEvent($serviceEvent);
301
        $this->checkServiceEventMethodName($serviceEvent, $eventMethodName);
302
303
        $serviceInstances = $this->getServicesFromEvent($serviceEvent);
304
305
        if (count($serviceInstances) > 0) {
306
            foreach ($serviceInstances as $serviceInstance) {
307
                $serviceInstance->$eventMethodName($dto);
308
            }
309
310
            $serviceInstances[0]->runDelayedCallbacks($dto);
311
        }
312
    }
313
314
    /**
315
     * Will check if the class of the service event is correct and implements
316
     * the correct interface.
317
     *
318
     * @param string $serviceEvent The class name of the service event.
319
     * @throws WrongInheritanceException
320
     */
321
    protected function checkServiceEvent($serviceEvent)
322
    {
323
        if (false === isset(self::$servicesChecked[$serviceEvent])) {
324
            self::$servicesChecked[$serviceEvent] = [];
325
326
            if (false === in_array(ServiceEventInterface::class, class_implements($serviceEvent))) {
327
                throw new WrongInheritanceException(
328
                    'Trying to run services with a wrong event: "' . $serviceEvent . '". Service events must extend "' . ServiceEventInterface::class . '".',
329
                    1456409155
330
                );
331
            }
332
        }
333
    }
334
335
    /**
336
     * Will check if the given method exists in the service event.
337
     *
338
     * @param string $serviceEvent    The class name of the service event.
339
     * @param string $eventMethodName Name of the method called in the service.
340
     * @throws MethodNotFoundException
341
     */
342
    protected function checkServiceEventMethodName($serviceEvent, $eventMethodName)
343
    {
344
        if (false === in_array($eventMethodName, self::$servicesChecked[$serviceEvent])) {
345
            /** @var ClassReflection $eventClassReflection */
346
            $eventClassReflection = GeneralUtility::makeInstance(ClassReflection::class, $serviceEvent);
347
            self::$servicesChecked[$serviceEvent][] = $eventMethodName;
348
349
            if (false === $eventClassReflection->hasMethod($eventMethodName)) {
350
                throw new MethodNotFoundException(
351
                    'The service event "' . $serviceEvent . '" does not have a method called "' . $eventMethodName . '".',
352
                    1456509926
353
                );
354
            }
355
        }
356
    }
357
358
    /**
359
     * Will loop trough all the services instance and get the ones which use the
360
     * given event.
361
     *
362
     * @param string $serviceEvent The class name of the service event.
363
     * @return AbstractService[]
364
     */
365
    protected function getServicesFromEvent($serviceEvent)
366
    {
367
        if (false === isset($this->servicesEvents[$serviceEvent])) {
368
            $servicesInstances = [];
369
            foreach ($this->serviceInstances as $serviceInstance) {
370
                if ($serviceInstance instanceof $serviceEvent) {
371
                    $servicesInstances[] = $serviceInstance;
372
                }
373
            }
374
375
            $this->servicesEvents[$serviceEvent] = $servicesInstances;
376
        }
377
378
        return $this->servicesEvents[$serviceEvent];
379
    }
380
381
    /**
382
     * This function is here to allow unit tests to override data.
383
     *
384
     * @param array $service
385
     * @return array
386
     * @internal
387
     */
388
    protected function manageServiceData(array $service)
389
    {
390
        return [$service['className'], $service['options']];
391
    }
392
393
    /**
394
     * @return string
395
     */
396
    public function getHash()
397
    {
398
        return spl_object_hash($this);
399
    }
400
}
401