Completed
Push — master ( 1df8f3...ce123c )
by Philip
07:02 queued 04:19
created

ServiceProvider::getEntities()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
nc 1
cc 1
eloc 2
nop 0
1
<?php
2
3
/*
4
 * This file is part of the CRUDlex package.
5
 *
6
 * (c) Philip Lehmann-Böhm <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace CRUDlex;
13
14
use Pimple\Container;
15
use Pimple\ServiceProviderInterface;
16
use Silex\Provider\LocaleServiceProvider;
17
use Silex\Provider\SessionServiceProvider;
18
use Silex\Provider\TranslationServiceProvider;
19
use Silex\Provider\TwigServiceProvider;
20
use Symfony\Component\Translation\Loader\YamlFileLoader;
21
use Symfony\Component\Yaml\Yaml;
22
23
/**
24
 * The ServiceProvider setups and initializes the whole CRUD system.
25
 * After adding it to your Silex-setup, it offers access to {@see AbstractData}
26
 * instances, one for each defined entity off the CRUD YAML file.
27
 */
28
class ServiceProvider implements ServiceProviderInterface {
29
30
    /**
31
     * Holds the {@see AbstractData} instances.
32
     */
33
    protected $datas;
34
35
    /**
36
     * Holds whether we manage the i18n.
37
     */
38
    protected $manageI18n;
39
40
    /**
41
     * Formats the given time value to a timestring defined by the $pattern
42
     * parameter.
43
     *
44
     * If the value is false (like null), an empty string is
45
     * returned. Else, the value is tried to be parsed as datetime via the
46
     * given pattern. If that fails, it is tried to be parsed with the pattern
47
     * 'Y-m-d H:i:s'. If that fails, the value is returned unchanged. Else, it
48
     * is returned formatted with the given pattern. The effect is to shorten
49
     * 'Y-m-d H:i:s' to 'Y-m-d' for example.
50
     *
51
     * @param string $value
52
     * the value to be formatted
53
     * @param string $timezone
54
     * the timezone of the value
55
     * @param string $pattern
56
     * the pattern with which the value is parsed and formatted
57
     *
58
     * @return string
59
     * the formatted value
60
     */
61
    protected function formatTime($value, $timezone, $pattern) {
62
        if (!$value) {
63
            return '';
64
        }
65
        $result = \DateTime::createFromFormat($pattern, $value, new \DateTimeZone($timezone));
66
        if ($result === false) {
67
            $result = \DateTime::createFromFormat('Y-m-d H:i:s', $value, new \DateTimeZone($timezone));
68
        }
69
        if ($result === false) {
70
            return $value;
71
        }
72
        $result->setTimezone(new \DateTimeZone(date_default_timezone_get()));
73
        return $result->format($pattern);
74
    }
75
76
    /**
77
     * Reads and returns the contents of the given Yaml file. If
78
     * it goes wrong, it throws an exception.
79
     *
80
     * @param string $fileName
81
     * the file to read
82
     *
83
     * @return array
84
     * the file contents
85
     *
86
     * @throws \RuntimeException
87
     * thrown if the file could not be read or parsed
88
     */
89
    protected function readYaml($fileName) {
90
        try {
91
            $fileContent = file_get_contents($fileName);
92
            $parsedYaml  = Yaml::parse($fileContent);
93
            if (!is_array($parsedYaml)) {
94
                $parsedYaml = [];
95
            }
96
            return $parsedYaml;
97
        } catch (\Exception $e) {
98
            throw new \RuntimeException('Could not open CRUD file '.$fileName, $e->getCode(), $e);
99
        }
100
    }
101
102
    /**
103
     * Initializes needed but yet missing service providers.
104
     *
105
     * @param Container $app
106
     * the application container
107
     */
108
    protected function initMissingServiceProviders(Container $app) {
109
110
        if (!$app->offsetExists('translator')) {
111
            $app->register(new LocaleServiceProvider());
112
            $app->register(new TranslationServiceProvider(), [
113
                'locale_fallbacks' => ['en'],
114
            ]);
115
        }
116
117
        if (!$app->offsetExists('session')) {
118
            $app->register(new SessionServiceProvider());
119
        }
120
121
        if (!$app->offsetExists('twig')) {
122
            $app->register(new TwigServiceProvider());
123
            $app['twig.loader.filesystem']->addPath(__DIR__.'/../views/', 'crud');
124
        }
125
    }
126
127
    /**
128
     * Initializes the available locales.
129
     *
130
     * @param Container $app
131
     * the application container
132
     *
133
     * @return array
134
     * the available locales
135
     */
136
    protected function initLocales(Container $app) {
137
        $app['translator']->addLoader('yaml', new YamlFileLoader());
138
        $localeDir = __DIR__.'/../locales';
139
        $locales   = $this->getLocales();
140
        foreach ($locales as $locale) {
141
            $app['translator']->addResource('yaml', $localeDir.'/'.$locale.'.yml', $locale);
142
        }
143
        return $locales;
144
    }
145
146
    /**
147
     * Initializes the children of the data entries.
148
     */
149
    protected function initChildren() {
150
        foreach ($this->datas as $name => $data) {
151
            $fields = $data->getDefinition()->getFieldNames();
152
            foreach ($fields as $field) {
153
                if ($data->getDefinition()->getType($field) == 'reference') {
154
                    $this->datas[$data->getDefinition()->getReferenceEntity($field)]->getDefinition()->addChild($data->getDefinition()->getTable(), $field, $name);
155
                }
156
            }
157
        }
158
    }
159
160
    /**
161
     * Gets a map with localized entity labels from the CRUD YML.
162
     *
163
     * @param array $locales
164
     * the available locales
165
     * @param array $crud
166
     * the CRUD entity map
167
     *
168
     * @return array
169
     * the map with localized entity labels
170
     */
171
    protected function getLocaleLabels($locales, $crud) {
172
        $localeLabels = [];
173
        foreach ($locales as $locale) {
174
            if (array_key_exists('label_'.$locale, $crud)) {
175
                $localeLabels[$locale] = $crud['label_'.$locale];
176
            }
177
        }
178
        return $localeLabels;
179
    }
180
181
    /**
182
     * Configures the EntityDefinition according to the given
183
     * CRUD entity map.
184
     *
185
     * @param EntityDefinition $definition
186
     * the definition to configure
187
     * @param array $crud
188
     * the CRUD entity map
189
     */
190
    protected function configureDefinition(EntityDefinition $definition, array $crud) {
191
        $toConfigure = [
192
            'deleteCascade',
193
            'listFields',
194
            'filter',
195
            'childrenLabelFields',
196
            'pageSize',
197
            'initialSortField',
198
            'initialSortAscending'
199
        ];
200
        foreach ($toConfigure as $field) {
201
            if (array_key_exists($field, $crud)) {
202
                $function = 'set'.ucfirst($field);
203
                $definition->$function($crud[$field]);
204
            }
205
        }
206
    }
207
208
    /**
209
     * Creates and setups an EntityDefinition instance.
210
     *
211
     * @param Container $app
212
     * the application container
213
     * @param array $locales
214
     * the available locales
215
     * @param array $crud
216
     * the parsed YAML of a CRUD entity
217
     * @param string $name
218
     * the name of the entity
219
     *
220
     * @return EntityDefinition
221
     * the EntityDefinition good to go
222
     */
223
    protected function createDefinition(Container $app, array $locales, array $crud, $name) {
224
        $label               = array_key_exists('label', $crud) ? $crud['label'] : $name;
225
        $localeLabels        = $this->getLocaleLabels($locales, $crud);
226
        $standardFieldLabels = [
227
            'id' => $app['translator']->trans('crudlex.label.id'),
228
            'created_at' => $app['translator']->trans('crudlex.label.created_at'),
229
            'updated_at' => $app['translator']->trans('crudlex.label.updated_at')
230
        ];
231
232
        $factory = $app->offsetExists('crud.entitydefinitionfactory') ? $app['crud.entitydefinitionfactory'] : new EntityDefinitionFactory();
233
234
        $definition = $factory->createEntityDefinition(
235
            $crud['table'],
236
            $crud['fields'],
237
            $label,
238
            $localeLabels,
239
            $standardFieldLabels,
240
            $this
241
        );
242
        $this->configureDefinition($definition, $crud);
243
        return $definition;
244
    }
245
246
    /**
247
     * Validates the parsed entity definition.
248
     *
249
     * @param Container $app
250
     * the application container
251
     * @param array $entityDefinition
252
     * the entity definition to validate
253
     */
254
    protected function validateEntityDefinition(Container $app, array $entityDefinition) {
255
        $doValidate = !$app->offsetExists('crud.validateentitydefinition') || $app['crud.validateentitydefinition'] === true;
256
        if ($doValidate) {
257
            $validator = $app->offsetExists('crud.entitydefinitionvalidator')
258
                ? $app['crud.entitydefinitionvalidator']
259
                : new EntityDefinitionValidator();
260
            $validator->validate($entityDefinition);
261
        }
262
    }
263
264
    /**
265
     * Initializes the instance.
266
     *
267
     * @param DataFactoryInterface $dataFactory
268
     * the factory to create the concrete AbstractData instances
269
     * @param string $crudFile
270
     * the CRUD YAML file to parse
271
     * @param FileProcessorInterface $fileProcessor
272
     * the file processor used for file fields
273
     * @param boolean $manageI18n
274
     * holds whether we manage the i18n
275
     * @param Container $app
276
     * the application container
277
     */
278
    public function init(DataFactoryInterface $dataFactory, $crudFile, FileProcessorInterface $fileProcessor, $manageI18n, Container $app) {
279
280
        $parsedYaml = $this->readYaml($crudFile);
281
282
        $this->validateEntityDefinition($app, $parsedYaml);
283
284
        $this->initMissingServiceProviders($app);
285
286
        $this->manageI18n = $manageI18n;
287
        $locales          = $this->initLocales($app);
288
        $this->datas      = [];
289
        foreach ($parsedYaml as $name => $crud) {
290
            $definition         = $this->createDefinition($app, $locales, $crud, $name);
291
            $this->datas[$name] = $dataFactory->createData($definition, $fileProcessor);
292
        }
293
294
        $twigExtensions = new TwigExtensions();
295
        $twigExtensions->registerTwigExtensions($app);
296
297
        $this->initChildren();
298
299
    }
300
301
    /**
302
     * Implements ServiceProviderInterface::register() registering $app['crud'].
303
     * $app['crud'] contains an instance of the ServiceProvider afterwards.
304
     *
305
     * @param Container $app
306
     * the Container instance of the Silex application
307
     */
308
    public function register(Container $app) {
309
        $app['crud'] = function() use ($app) {
310
            $result        = new static();
311
            $fileProcessor = $app->offsetExists('crud.fileprocessor') ? $app['crud.fileprocessor'] : new SimpleFilesystemFileProcessor();
312
            $manageI18n    = $app->offsetExists('crud.manageI18n') ? $app['crud.manageI18n'] : true;
313
            $result->init($app['crud.datafactory'], $app['crud.file'], $fileProcessor, $manageI18n, $app);
314
            return $result;
315
        };
316
    }
317
318
    /**
319
     * Getter for the {@see AbstractData} instances.
320
     *
321
     * @param string $name
322
     * the entity name of the desired Data instance
323
     *
324
     * @return AbstractData
325
     * the AbstractData instance or null on invalid name
326
     */
327
    public function getData($name) {
328
        if (!array_key_exists($name, $this->datas)) {
329
            return null;
330
        }
331
        return $this->datas[$name];
332
    }
333
334
    /**
335
     * Getter for all available entity names.
336
     *
337
     * @return string[]
338
     * a list of all available entity names
339
     */
340
    public function getEntities() {
341
        return array_keys($this->datas);
342
    }
343
344
    /**
345
     * Formats the given value to a date of the format 'Y-m-d'.
346
     *
347
     * @param string $value
348
     * the value, might be of the format 'Y-m-d H:i' or 'Y-m-d'
349
     * @param boolean $isUTC
350
     * whether the given value is in UTC
351
     *
352
     * @return string
353
     * the formatted result or an empty string on null value
354
     */
355
    public function formatDate($value, $isUTC) {
356
        $timezone = $isUTC ? 'UTC' : date_default_timezone_get();
357
        return $this->formatTime($value, $timezone, 'Y-m-d');
358
    }
359
360
    /**
361
     * Formats the given value to a date of the format 'Y-m-d H:i'.
362
     *
363
     * @param string $value
364
     * the value, might be of the format 'Y-m-d H:i'
365
     * @param boolean $isUTC
366
     * whether the given value is in UTC
367
     *
368
     * @return string
369
     * the formatted result or an empty string on null value
370
     */
371
    public function formatDateTime($value, $isUTC) {
372
        $timezone = $isUTC ? 'UTC' : date_default_timezone_get();
373
        return $this->formatTime($value, $timezone, 'Y-m-d H:i');
374
    }
375
376
    /**
377
     * Calls PHPs
378
     * {@link http://php.net/manual/en/function.basename.php basename} and
379
     * returns it's result.
380
     *
381
     * @param string $value
382
     * the value to be handed to basename
383
     *
384
     * @return string
385
     * the result of basename
386
     */
387
    public function basename($value) {
388
        return basename($value);
389
    }
390
391
    /**
392
     * Determines the Twig template to use for the given parameters depending on
393
     * the existance of certain keys in the Container $app in this order:
394
     *
395
     * crud.$section.$action.$entity
396
     * crud.$section.$action
397
     * crud.$section
398
     *
399
     * If nothing exists, this string is returned: "@crud/<action>.twig"
400
     *
401
     * @param Container $app
402
     * the Silex application
403
     * @param string $section
404
     * the section of the template, either "layout" or "template"
405
     * @param string $action
406
     * the current calling action like "create" or "show"
407
     * @param string $entity
408
     * the current calling entity
409
     *
410
     * @return string
411
     * the best fitting template
412
     */
413
    public function getTemplate(Container $app, $section, $action, $entity) {
414
        $crudSection       = 'crud.'.$section;
415
        $crudSectionAction = $crudSection.'.'.$action;
416
417
        $offsets = [
418
            $crudSectionAction.'.'.$entity,
419
            $crudSection.'.'.$entity,
420
            $crudSectionAction,
421
            $crudSection
422
        ];
423
        foreach ($offsets as $offset) {
424
            if ($app->offsetExists($offset)) {
425
                return $app[$offset];
426
            }
427
        }
428
429
        return '@crud/'.$action.'.twig';
430
    }
431
432
    /**
433
     * Gets whether CRUDlex manages the i18n system.
434
     *
435
     * @return boolean
436
     * true if CRUDlex manages the i18n system
437
     */
438
    public function isManagingI18n() {
439
        return $this->manageI18n;
440
    }
441
442
    /**
443
     * Sets the locale to be used.
444
     *
445
     * @param string $locale
446
     * the locale to be used.
447
     */
448
    public function setLocale($locale) {
449
        foreach ($this->datas as $data) {
450
            $data->getDefinition()->setLocale($locale);
451
        }
452
    }
453
454
    /**
455
     * Gets the available locales.
456
     *
457
     * @return array
458
     * the available locales
459
     */
460
    public function getLocales() {
461
        $localeDir     = __DIR__.'/../locales';
462
        $languageFiles = scandir($localeDir);
463
        $locales       = [];
464
        foreach ($languageFiles as $languageFile) {
465
            if (in_array($languageFile, ['.', '..'])) {
466
                continue;
467
            }
468
            $extensionPos = strpos($languageFile, '.yml');
469
            if ($extensionPos !== false) {
470
                $locale    = substr($languageFile, 0, $extensionPos);
471
                $locales[] = $locale;
472
            }
473
        }
474
        sort($locales);
475
        return $locales;
476
    }
477
478
    /**
479
     * Formats a float to not display in scientific notation.
480
     *
481
     * @param float $float
482
     * the float to format
483
     *
484
     * @return double|string
485
     * the formated float
486
     */
487
    public function formatFloat($float) {
488
489
        if (!$float) {
490
            return $float;
491
        }
492
493
        $zeroFraction = $float - floor($float) == 0 ? '0' : '';
494
495
        // We don't want values like 0.004 converted to  0.00400000000000000008
496
        if ($float > 0.0001) {
497
            return $float.($zeroFraction === '0' ? '.'.$zeroFraction : '');
498
        }
499
500
        // We don't want values like 0.00004 converted to its scientific notation 4.0E-5
501
        return rtrim(sprintf('%.20F', $float), '0').$zeroFraction;
502
    }
503
504
}
505