Completed
Push — master ( d97cdb...b6818f )
by Philip
05:04 queued 02:29
created

src/CRUDlex/Service.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 League\Flysystem\FilesystemInterface;
15
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16
use Symfony\Component\Translation\Loader\YamlFileLoader;
17
use Symfony\Component\Translation\TranslatorInterface;
18
19
/**
20
 * The Service setups and initializes the whole CRUD system and is initialized via the framework
21
 * specific implementation, the Silex one for example.
22
 * It offers access to AbstractData instances, one for each defined entity off the CRUD YAML file
23
 * and various other helper functions.
24
 */
25
class Service
26
{
27
28
    /**
29
     * Holds the data instances.
30
     * @var array
31
     */
32
    protected $datas;
33
34
    /**
35
     * Holds the map for overriding templates.
36
     * @var array
37
     */
38
    protected $templates = [];
39
40
    /**
41
     * Holds whether CRUDlex manages i18n.
42
     * @var bool
43
     */
44
    protected $manageI18n = true;
45
46
    /**
47
     * Holds the URL generator.
48
     * @var \Symfony\Component\Routing\Generator\UrlGenerator
49
     */
50
    protected $urlGenerator;
51
52
    /**
53
     * Initializes the available locales.
54
     *
55
     * @param TranslatorInterface $translator
56
     * the translator
57
     *
58
     * @return array
59
     * the available locales
60
     */
61 80
    protected function initLocales(TranslatorInterface $translator)
62
    {
63 80
        $locales   = $this->getLocales();
64 80
        $localeDir = __DIR__.'/../locales';
65 80
        $translator->addLoader('yaml', new YamlFileLoader());
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Symfony\Component\Translation\TranslatorInterface as the method addLoader() does only exist in the following implementations of said interface: Symfony\Component\Translation\Translator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
66 80
        foreach ($locales as $locale) {
67 80
            $translator->addResource('yaml', $localeDir.'/'.$locale.'.yml', $locale);
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Symfony\Component\Translation\TranslatorInterface as the method addResource() does only exist in the following implementations of said interface: Symfony\Component\Translation\Translator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
68
        }
69 80
        return $locales;
70
    }
71
72
    /**
73
     * Initializes the children of the data entries.
74
     */
75 80
    protected function initChildren()
76
    {
77 80
        foreach ($this->datas as $name => $data) {
78 80
            $fields = $data->getDefinition()->getFieldNames();
79 80
            foreach ($fields as $field) {
80 80
                if ($data->getDefinition()->getType($field) == 'reference') {
81 80
                    $this->datas[$data->getDefinition()->getSubTypeField($field, 'reference', 'entity')]->getDefinition()->addChild($data->getDefinition()->getTable(), $field, $name);
82
                }
83
            }
84
        }
85 80
    }
86
87
    /**
88
     * Gets a map with localized entity labels from the CRUD YML.
89
     *
90
     * @param array $locales
91
     * the available locales
92
     * @param array $crud
93
     * the CRUD entity map
94
     *
95
     * @return array
96
     * the map with localized entity labels
97
     */
98 80
    protected function getLocaleLabels(array $locales, array $crud)
99
    {
100 80
        $localeLabels = [];
101 80
        foreach ($locales as $locale) {
102 80
            if (array_key_exists('label_'.$locale, $crud)) {
103 80
                $localeLabels[$locale] = $crud['label_'.$locale];
104
            }
105
        }
106 80
        return $localeLabels;
107
    }
108
109
    /**
110
     * Configures the EntityDefinition according to the given
111
     * CRUD entity map.
112
     *
113
     * @param EntityDefinition $definition
114
     * the definition to configure
115
     * @param array $crud
116
     * the CRUD entity map
117
     */
118 80
    protected function configureDefinition(EntityDefinition $definition, array $crud)
119
    {
120
        $toConfigure = [
121 80
            'deleteCascade',
122
            'listFields',
123
            'filter',
124
            'childrenLabelFields',
125
            'pageSize',
126
            'initialSortField',
127
            'initialSortAscending',
128
            'navBarGroup',
129
            'optimisticLocking',
130
            'hardDeletion',
131
        ];
132 80
        foreach ($toConfigure as $field) {
133 80
            if (array_key_exists($field, $crud)) {
134 80
                $function = 'set'.ucfirst($field);
135 80
                $definition->$function($crud[$field]);
136
            }
137
        }
138 80
    }
139
140
    /**
141
     * Creates and setups an EntityDefinition instance.
142
     *
143
     * @param TranslatorInterface $translator
144
     * the Translator to use for some standard field labels
145
     * @param EntityDefinitionFactoryInterface $entityDefinitionFactory
146
     * the EntityDefinitionFactory to use
147
     * @param array $locales
148
     * the available locales
149
     * @param array $crud
150
     * the parsed YAML of a CRUD entity
151
     * @param string $name
152
     * the name of the entity
153
     *
154
     * @return EntityDefinition
155
     * the EntityDefinition good to go
156
     */
157 80
    protected function createDefinition(TranslatorInterface $translator, EntityDefinitionFactoryInterface $entityDefinitionFactory, array $locales, array $crud, $name)
158
    {
159 80
        $label               = array_key_exists('label', $crud) ? $crud['label'] : $name;
160 80
        $localeLabels        = $this->getLocaleLabels($locales, $crud);
161
        $standardFieldLabels = [
162 80
            'id' => $translator->trans('crudlex.label.id'),
163 80
            'created_at' => $translator->trans('crudlex.label.created_at'),
164 80
            'updated_at' => $translator->trans('crudlex.label.updated_at')
165
        ];
166
167 80
        $definition = $entityDefinitionFactory->createEntityDefinition(
168 80
            $crud['table'],
169 80
            $crud['fields'],
170 80
            $label,
171 80
            $localeLabels,
172 80
            $standardFieldLabels,
173 80
            $this
174
        );
175 80
        $this->configureDefinition($definition, $crud);
176 80
        return $definition;
177
    }
178
179
    /**
180
     * Initializes the instance.
181
     *
182
     * @param string $crudFile
183
     * the CRUD YAML file
184
     * @param string|null $crudFileCachingDirectory
185
     * the writable directory to store the CRUD YAML file cache
186
     * @param UrlGeneratorInterface $urlGenerator
187
     * the URL generator to use
188
     * @param TranslatorInterface $translator
189
     * the translator to use
190
     * @param DataFactoryInterface $dataFactory
191
     * the data factory to use
192
     * @param EntityDefinitionFactoryInterface $entityDefinitionFactory
193
     * the EntityDefinitionFactory to use
194
     * @param FilesystemInterface $filesystem
195
     * the filesystem to use
196
     * @param EntityDefinitionValidatorInterface|null $validator
197
     * the validator to use, null if no validation required
198
     */
199 82
    public function __construct($crudFile, $crudFileCachingDirectory, UrlGeneratorInterface $urlGenerator, TranslatorInterface $translator, DataFactoryInterface $dataFactory, EntityDefinitionFactoryInterface $entityDefinitionFactory, FilesystemInterface $filesystem, ?EntityDefinitionValidatorInterface $validator)
200
    {
201
202 82
        $this->urlGenerator = $urlGenerator;
0 ignored issues
show
Documentation Bug introduced by
$urlGenerator is of type object<Symfony\Component...\UrlGeneratorInterface>, but the property $urlGenerator was declared to be of type object<Symfony\Component...Generator\UrlGenerator>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
203
204 82
        $reader     = new YamlReader($crudFileCachingDirectory);
205 82
        $parsedYaml = $reader->read($crudFile);
206
207 80
        if ($validator !== null) {
208 80
            $validator->validate($parsedYaml);
209
        }
210
211 80
        $locales     = $this->initLocales($translator);
212 80
        $this->datas = [];
213 80
        foreach ($parsedYaml as $name => $crud) {
214 80
            $definition         = $this->createDefinition($translator, $entityDefinitionFactory, $locales, $crud, $name);
215 80
            $this->datas[$name] = $dataFactory->createData($definition, $filesystem);
216
        }
217
218 80
        $this->initChildren();
219
220 80
    }
221
222
    /**
223
     * Getter for the AbstractData instances.
224
     *
225
     * @param string $name
226
     * the entity name of the desired Data instance
227
     *
228
     * @return AbstractData
229
     * the AbstractData instance or null on invalid name
230
     */
231 71
    public function getData($name)
232
    {
233 71
        if (!array_key_exists($name, $this->datas)) {
234 7
            return null;
235
        }
236 71
        return $this->datas[$name];
237
    }
238
239
    /**
240
     * Getter for all available entity names.
241
     *
242
     * @return string[]
243
     * a list of all available entity names
244
     */
245 7
    public function getEntities()
246
    {
247 7
        return array_keys($this->datas);
248
    }
249
250
    /**
251
     * Getter for the entities for the navigation bar.
252
     *
253
     * @return string[]
254
     * a list of all available entity names with their group
255
     */
256 11
    public function getEntitiesNavBar()
257
    {
258 11
        $result = [];
259 11
        foreach ($this->datas as $entity => $data) {
260 11
            $navBarGroup = $data->getDefinition()->getNavBarGroup();
261 11
            if ($navBarGroup !== 'main') {
262 11
                $result[$navBarGroup][] = $entity;
263
            } else {
264 11
                $result[$entity] = 'main';
265
            }
266
        }
267 11
        return $result;
268
    }
269
270
    /**
271
     * Sets a template to use instead of the build in ones.
272
     *
273
     * @param string $key
274
     * the template key to use in this format:
275
     * $section.$action.$entity
276
     * $section.$action
277
     * $section
278
     * @param string $template
279
     * the template to use for this key
280
     */
281 13
    public function setTemplate($key, $template)
282
    {
283 13
        $this->templates[$key] = $template;
284 13
    }
285
286
    /**
287
     * Determines the Twig template to use for the given parameters depending on
288
     * the existance of certain template keys set in this order:
289
     *
290
     * $section.$action.$entity
291
     * $section.$action
292
     * $section
293
     *
294
     * If nothing exists, this string is returned: "@crud/<action>.twig"
295
     *
296
     * @param string $section
297
     * the section of the template, either "layout" or "template"
298
     * @param string $action
299
     * the current calling action like "create" or "show"
300
     * @param string $entity
301
     * the current calling entity
302
     *
303
     * @return string
304
     * the best fitting template
305
     */
306 12
    public function getTemplate($section, $action, $entity)
307
    {
308 12
        $sectionAction = $section.'.'.$action;
309
310
        $offsets = [
311 12
            $sectionAction.'.'.$entity,
312 12
            $section.'.'.$entity,
313 12
            $sectionAction,
314 12
            $section
315
        ];
316 12
        foreach ($offsets as $offset) {
317 12
            if (array_key_exists($offset, $this->templates)) {
318 12
                return $this->templates[$offset];
319
            }
320
        }
321
322 12
        return '@crud/'.$action.'.twig';
323
    }
324
325
    /**
326
     * Sets the locale to be used.
327
     *
328
     * @param string $locale
329
     * the locale to be used.
330
     */
331 9
    public function setLocale($locale)
332
    {
333 9
        foreach ($this->datas as $data) {
334 9
            $data->getDefinition()->setLocale($locale);
335
        }
336 9
    }
337
338
    /**
339
     * Gets the available locales.
340
     *
341
     * @return array
342
     * the available locales
343
     */
344 80
    public function getLocales()
345
    {
346 80
        $localeDir     = __DIR__.'/../locales';
347 80
        $languageFiles = scandir($localeDir);
348 80
        $locales       = [];
349 80
        foreach ($languageFiles as $languageFile) {
350 80
            if (in_array($languageFile, ['.', '..'])) {
351 80
                continue;
352
            }
353 80
            $extensionPos = strpos($languageFile, '.yml');
354 80
            if ($extensionPos !== false) {
355 80
                $locale    = substr($languageFile, 0, $extensionPos);
356 80
                $locales[] = $locale;
357
            }
358
        }
359 80
        sort($locales);
360 80
        return $locales;
361
    }
362
363
    /**
364
     * Gets whether CRUDlex manages the i18n.
365
     * @return bool
366
     * true if so
367
     */
368 12
    public function isManageI18n()
369
    {
370 12
        return $this->manageI18n;
371
    }
372
373
    /**
374
     * Sets whether CRUDlex manages the i18n.
375
     * @param bool $manageI18n
376
     * true if so
377
     */
378 1
    public function setManageI18n($manageI18n)
379
    {
380 1
        $this->manageI18n = $manageI18n;
381 1
    }
382
383
    /**
384
     * Generates an URL.
385
     * @param string $name
386
     * the name of the route
387
     * @param mixed $parameters
388
     * an array of parameters
389
     * @return null|string
390
     * the generated URL
391
     */
392 12
    public function generateURL($name, $parameters)
393
    {
394 12
        return $this->urlGenerator->generate($name, $parameters);
395
    }
396
397
}
398