Completed
Pull Request — master (#85)
by
unknown
02:47
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\Api\BootableProviderInterface;
17
use Silex\Application;
18
use Silex\Provider\LocaleServiceProvider;
19
use Silex\Provider\SessionServiceProvider;
20
use Silex\Provider\TranslationServiceProvider;
21
use Silex\Provider\TwigServiceProvider;
22
use Symfony\Component\Translation\Loader\YamlFileLoader;
23
use Symfony\Component\Yaml\Yaml;
24
25
/**
26
 * The ServiceProvider setups and initializes the whole CRUD system.
27
 * After adding it to your Silex-setup, it offers access to {@see AbstractData}
28
 * instances, one for each defined entity off the CRUD YAML file.
29
 */
30
class ServiceProvider implements ServiceProviderInterface, BootableProviderInterface {
31
32
    /**
33
     * Holds the {@see AbstractData} instances.
34
     */
35
    protected $datas;
36
37
    /**
38
     * Reads and returns the contents of the given Yaml file. If
39
     * it goes wrong, it throws an exception.
40
     *
41
     * @param string $fileName
42
     * the file to read
43
     *
44
     * @return array
45
     * the file contents
46
     *
47
     * @throws \RuntimeException
48
     * thrown if the file could not be read or parsed
49
     */
50
    protected function readYaml($fileName) {
51
        try {
52
            $fileContent = file_get_contents($fileName);
53
            $parsedYaml  = Yaml::parse($fileContent);
54
            if (!is_array($parsedYaml)) {
55
                $parsedYaml = [];
56
            }
57
            return $parsedYaml;
58
        } catch (\Exception $e) {
59
            throw new \RuntimeException('Could not open CRUD file '.$fileName, $e->getCode(), $e);
60
        }
61
    }
62
63
    /**
64
     * Initializes needed but yet missing service providers.
65
     *
66
     * @param Container $app
67
     * the application container
68
     */
69
    protected function initMissingServiceProviders(Container $app) {
70
71
        if (!$app->offsetExists('translator')) {
72
            $app->register(new LocaleServiceProvider());
73
            $app->register(new TranslationServiceProvider(), [
74
                'locale_fallbacks' => ['en'],
75
            ]);
76
        }
77
78
        if (!$app->offsetExists('session')) {
79
            $app->register(new SessionServiceProvider());
80
        }
81
82
        if (!$app->offsetExists('twig')) {
83
            $app->register(new TwigServiceProvider());
84
        }
85
        $app['twig.loader.filesystem']->addPath(__DIR__.'/../views/', 'crud');
86
    }
87
88
    /**
89
     * Initializes the available locales.
90
     *
91
     * @param Container $app
92
     * the application container
93
     *
94
     * @return array
95
     * the available locales
96
     */
97
    protected function initLocales(Container $app) {
98
        $locales   = $this->getLocales();
99
        $localeDir = __DIR__.'/../locales';
100
        $app['translator']->addLoader('yaml', new YamlFileLoader());
101
        foreach ($locales as $locale) {
102
            $app['translator']->addResource('yaml', $localeDir.'/'.$locale.'.yml', $locale);
103
        }
104
        return $locales;
105
    }
106
107
    /**
108
     * Initializes the children of the data entries.
109
     */
110
    protected function initChildren() {
111
        foreach ($this->datas as $name => $data) {
112
            $fields = $data->getDefinition()->getFieldNames();
113
            foreach ($fields as $field) {
114
                if ($data->getDefinition()->getType($field) == 'reference') {
115
                    $this->datas[$data->getDefinition()->getSubTypeField($field, 'reference', 'entity')]->getDefinition()->addChild($data->getDefinition()->getTable(), $field, $name);
116
                }
117
            }
118
        }
119
    }
120
121
    /**
122
     * Gets a map with localized entity labels from the CRUD YML.
123
     *
124
     * @param array $locales
125
     * the available locales
126
     * @param array $crud
127
     * the CRUD entity map
128
     *
129
     * @return array
130
     * the map with localized entity labels
131
     */
132
    protected function getLocaleLabels($locales, $crud) {
133
        $localeLabels = [];
134
        foreach ($locales as $locale) {
135
            if (array_key_exists('label_'.$locale, $crud)) {
136
                $localeLabels[$locale] = $crud['label_'.$locale];
137
            }
138
        }
139
        return $localeLabels;
140
    }
141
142
    /**
143
     * Configures the EntityDefinition according to the given
144
     * CRUD entity map.
145
     *
146
     * @param EntityDefinition $definition
147
     * the definition to configure
148
     * @param array $crud
149
     * the CRUD entity map
150
     */
151
    protected function configureDefinition(EntityDefinition $definition, array $crud) {
152
        $toConfigure = [
153
            'deleteCascade',
154
            'listFields',
155
            'filter',
156
            'childrenLabelFields',
157
            'pageSize',
158
            'initialSortField',
159
            'initialSortAscending',
160
            'navBarGroup'
161
        ];
162
        foreach ($toConfigure as $field) {
163
            if (array_key_exists($field, $crud)) {
164
                $function = 'set'.ucfirst($field);
165
                $definition->$function($crud[$field]);
166
            }
167
        }
168
    }
169
170
    /**
171
     * Creates and setups an EntityDefinition instance.
172
     *
173
     * @param Container $app
174
     * the application container
175
     * @param array $locales
176
     * the available locales
177
     * @param array $crud
178
     * the parsed YAML of a CRUD entity
179
     * @param string $name
180
     * the name of the entity
181
     *
182
     * @return EntityDefinition
183
     * the EntityDefinition good to go
184
     */
185
    protected function createDefinition(Container $app, array $locales, array $crud, $name) {
186
        $label               = array_key_exists('label', $crud) ? $crud['label'] : $name;
187
        $localeLabels        = $this->getLocaleLabels($locales, $crud);
188
        $standardFieldLabels = [
189
            'id' => $app['translator']->trans('crudlex.label.id'),
190
            'created_at' => $app['translator']->trans('crudlex.label.created_at'),
191
            'updated_at' => $app['translator']->trans('crudlex.label.updated_at')
192
        ];
193
194
        $factory = $app->offsetExists('crud.entitydefinitionfactory') ? $app['crud.entitydefinitionfactory'] : new EntityDefinitionFactory();
195
196
        $definition = $factory->createEntityDefinition(
197
            $crud['table'],
198
            $crud['fields'],
199
            $label,
200
            $localeLabels,
201
            $standardFieldLabels,
202
            $this
203
        );
204
        $this->configureDefinition($definition, $crud);
205
        return $definition;
206
    }
207
208
    /**
209
     * Validates the parsed entity definition.
210
     *
211
     * @param Container $app
212
     * the application container
213
     * @param array $entityDefinition
214
     * the entity definition to validate
215
     */
216
    protected function validateEntityDefinition(Container $app, array $entityDefinition) {
217
        $doValidate = !$app->offsetExists('crud.validateentitydefinition') || $app['crud.validateentitydefinition'] === true;
218
        if ($doValidate) {
219
            $validator = $app->offsetExists('crud.entitydefinitionvalidator')
220
                ? $app['crud.entitydefinitionvalidator']
221
                : new EntityDefinitionValidator();
222
            $validator->validate($entityDefinition);
223
        }
224
    }
225
226
    /**
227
     * Initializes the instance.
228
     *
229
     * @param DataFactoryInterface $dataFactory
230
     * the factory to create the concrete AbstractData instances
231
     * @param string $crudFile
232
     * the CRUD YAML file to parse
233
     * @param FileProcessorInterface $fileProcessor
234
     * the file processor used for file fields
235
     * @param Container $app
236
     * the application container
237
     */
238
    public function init(DataFactoryInterface $dataFactory, $crudFile, FileProcessorInterface $fileProcessor, Container $app) {
239
240
        $parsedYaml = $this->readYaml($crudFile);
241
242
        $this->validateEntityDefinition($app, $parsedYaml);
243
244
        $locales     = $this->initLocales($app);
245
        $this->datas = [];
246
        foreach ($parsedYaml as $name => $crud) {
247
            $definition         = $this->createDefinition($app, $locales, $crud, $name);
248
            $this->datas[$name] = $dataFactory->createData($definition, $fileProcessor);
249
        }
250
251
        $this->initChildren();
252
253
    }
254
255
    /**
256
     * Implements ServiceProviderInterface::register() registering $app['crud'].
257
     * $app['crud'] contains an instance of the ServiceProvider afterwards.
258
     *
259
     * @param Container $app
260
     * the Container instance of the Silex application
261
     */
262
    public function register(Container $app) {
263
        $app['crud'] = function() use ($app) {
264
            $result        = new static();
265
            $fileProcessor = $app->offsetExists('crud.fileprocessor') ? $app['crud.fileprocessor'] : new SimpleFilesystemFileProcessor();
266
            $result->init($app['crud.datafactory'], $app['crud.file'], $fileProcessor, $app);
267
            return $result;
268
        };
269
    }
270
271
    /**
272
     * Initializes the crud service right after boot.
273
     *
274
     * @param Application $app
275
     * the Container instance of the Silex application
276
     */
277
    public function boot(Application $app) {
278
        $this->initMissingServiceProviders($app);
279
        $twigExtensions = new TwigExtensions();
280
        $twigExtensions->registerTwigExtensions($app);
281
    }
282
283
    /**
284
     * Getter for the {@see AbstractData} instances.
285
     *
286
     * @param string $name
287
     * the entity name of the desired Data instance
288
     *
289
     * @return AbstractData
290
     * the AbstractData instance or null on invalid name
291
     */
292
    public function getData($name) {
293
        if (!array_key_exists($name, $this->datas)) {
294
            return null;
295
        }
296
        return $this->datas[$name];
297
    }
298
299
    /**
300
     * Getter for all available entity names.
301
     *
302
     * @return string[]
303
     * a list of all available entity names
304
     */
305
    public function getEntities() {
306
        return array_keys($this->datas);
307
    }
308
309
    /**
310
     * Getter for the entities fot the navigation bar.
311
     *
312
     * @return string[]
0 ignored issues
show
Documentation introduced by
Should the return type not be array<*,string>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
313
     * a list of all available entity names with their group
314
     */
315
    public function getEntitiesNavBar() {
316
        foreach ($this->datas as $entity => $data) {
317
            $navBarGroup = $data->getDefinition()->getNavBarGroup();
318
            if ($navBarGroup !== 'main'){
319
                $result[$navBarGroup][] = $entity;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
320
            }else{
321
                $result[$entity] = 'main';
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
322
            }
323
        }
324
        return $result;
325
    }
326
327
    /**
328
     * Determines the Twig template to use for the given parameters depending on
329
     * the existance of certain keys in the Container $app in this order:
330
     *
331
     * crud.$section.$action.$entity
332
     * crud.$section.$action
333
     * crud.$section
334
     *
335
     * If nothing exists, this string is returned: "@crud/<action>.twig"
336
     *
337
     * @param Container $app
338
     * the Silex application
339
     * @param string $section
340
     * the section of the template, either "layout" or "template"
341
     * @param string $action
342
     * the current calling action like "create" or "show"
343
     * @param string $entity
344
     * the current calling entity
345
     *
346
     * @return string
347
     * the best fitting template
348
     */
349
    public function getTemplate(Container $app, $section, $action, $entity) {
350
        $crudSection       = 'crud.'.$section;
351
        $crudSectionAction = $crudSection.'.'.$action;
352
353
        $offsets = [
354
            $crudSectionAction.'.'.$entity,
355
            $crudSection.'.'.$entity,
356
            $crudSectionAction,
357
            $crudSection
358
        ];
359
        foreach ($offsets as $offset) {
360
            if ($app->offsetExists($offset)) {
361
                return $app[$offset];
362
            }
363
        }
364
365
        return '@crud/'.$action.'.twig';
366
    }
367
368
    /**
369
     * Sets the locale to be used.
370
     *
371
     * @param string $locale
372
     * the locale to be used.
373
     */
374
    public function setLocale($locale) {
375
        foreach ($this->datas as $data) {
376
            $data->getDefinition()->setLocale($locale);
377
        }
378
    }
379
380
    /**
381
     * Gets the available locales.
382
     *
383
     * @return array
384
     * the available locales
385
     */
386
    public function getLocales() {
387
        $localeDir     = __DIR__.'/../locales';
388
        $languageFiles = scandir($localeDir);
389
        $locales       = [];
390
        foreach ($languageFiles as $languageFile) {
391
            if (in_array($languageFile, ['.', '..'])) {
392
                continue;
393
            }
394
            $extensionPos = strpos($languageFile, '.yml');
395
            if ($extensionPos !== false) {
396
                $locale    = substr($languageFile, 0, $extensionPos);
397
                $locales[] = $locale;
398
            }
399
        }
400
        sort($locales);
401
        return $locales;
402
    }
403
404
}
405