├── .circleci
└── config.yml
├── .gitignore
├── Command
├── ExportCommand.php
└── ImportCommand.php
├── Controller
└── ParameterController.php
├── DependencyInjection
├── Compiler
│ └── FieldTypePass.php
├── Configuration.php
└── SherlockodeConfigurationExtension.php
├── Event
├── PostSaveEvent.php
├── PreSaveEvent.php
├── RequestEventTrait.php
├── ResponseEventTrait.php
└── SaveEvent.php
├── FieldType
├── AbstractField.php
├── CheckboxField.php
├── ChoiceField.php
├── DateTimeField.php
├── EntityField.php
├── FieldTypeInterface.php
├── ImageField.php
├── PasswordField.php
└── SimpleField.php
├── Form
└── Type
│ ├── ImageType.php
│ ├── ImportType.php
│ └── ParametersType.php
├── LICENSE
├── Manager
├── ConfigurationManager.php
├── ConfigurationManagerInterface.php
├── ConstraintManager.php
├── ConstraintManagerInterface.php
├── ExportManager.php
├── ExportManagerInterface.php
├── FieldTypeManager.php
├── FieldTypeManagerInterface.php
├── ImportManager.php
├── ImportManagerInterface.php
├── ParameterConverter.php
├── ParameterConverterInterface.php
├── ParameterManager.php
├── ParameterManagerInterface.php
├── UploadManager.php
└── UploadManagerInterface.php
├── Model
├── Parameter.php
└── ParameterInterface.php
├── Parameter
└── ParameterDefinition.php
├── README.md
├── Resources
├── config
│ ├── field_types.xml
│ ├── routing.xml
│ └── services.xml
├── translations
│ ├── SherlockodeConfigurationBundle.en.yml
│ └── SherlockodeConfigurationBundle.fr.yml
└── views
│ ├── base.html.twig
│ ├── form.html.twig
│ ├── import.html.twig
│ └── parameters.html.twig
├── SherlockodeConfigurationBundle.php
├── Tests
├── ConfigurationManagerTest.php
├── FieldTypeManagerTest.php
└── Transformer
│ ├── ArrayTransformerTest.php
│ ├── BooleanTransformerTest.php
│ └── CallbackTransformerTest.php
├── Transformer
├── ArrayTransformer.php
├── BooleanTransformer.php
├── CallbackTransformer.php
└── TransformerInterface.php
├── Twig
└── ParameterExtension.php
├── composer.json
└── phpunit.xml.dist
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | common: &common
4 | environment:
5 | SYMFONY_DEPRECATIONS_HELPER: disabled
6 | APP_ENV: test
7 | steps:
8 | - checkout
9 | - run: composer install --no-scripts
10 | - run: php ./vendor/bin/phpunit
11 |
12 |
13 | jobs:
14 | test80:
15 | docker:
16 | - image: cimg/php:8.0
17 | <<: *common
18 | test81:
19 | docker:
20 | - image: cimg/php:8.1
21 | <<: *common
22 | workflows:
23 | version: 2
24 | build:
25 | jobs:
26 | - test80
27 | - test81
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | vendor
3 | phpunit.xml
4 |
--------------------------------------------------------------------------------
/Command/ExportCommand.php:
--------------------------------------------------------------------------------
1 | exportManager = $exportManager;
25 | }
26 |
27 | protected function execute(InputInterface $input, OutputInterface $output): int
28 | {
29 | $io = new SymfonyStyle($input, $output);
30 | $io->info('Start exporting configuration...');
31 | $this->exportManager->exportInVault();
32 | $io->success('Configuration successfully sealed in vault');
33 |
34 | return Command::SUCCESS;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Command/ImportCommand.php:
--------------------------------------------------------------------------------
1 | importManager = $importManager;
25 | }
26 |
27 | protected function execute(InputInterface $input, OutputInterface $output): int
28 | {
29 | $io = new SymfonyStyle($input, $output);
30 | $io->info('Start importing configuration...');
31 | $this->importManager->importFromVault();
32 | $io->success('Configuration successfully imported from vault');
33 |
34 | return Command::SUCCESS;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Controller/ParameterController.php:
--------------------------------------------------------------------------------
1 | parameterManager = $parameterManager;
71 | $this->exportManager = $exportManager;
72 | $this->importManager = $importManager;
73 | $this->eventDispatcher = $eventDispatcher;
74 | $this->requestStack = $requestStack;
75 | $this->formFactory = $formFactory;
76 | $this->urlGenerator = $urlGenerator;
77 | $this->twig = $twig;
78 | $this->editFormTemplate = $editFormTemplate;
79 | $this->importFormTemplate = $importFormTemplate;
80 | $this->redirectAfterImportRoute = $redirectAfterImportRoute;
81 | }
82 |
83 | /**
84 | * @throws LoaderError
85 | * @throws RuntimeError
86 | * @throws SyntaxError
87 | */
88 | public function listAction(Request $request): Response
89 | {
90 | $form = $this->formFactory->create(ParametersType::class, $this->parameterManager->getAll());
91 | $form->handleRequest($request);
92 |
93 | if ($form->isSubmitted() && $form->isValid()) {
94 | $params = $form->getData();
95 |
96 | $preSaveEvent = new PreSaveEvent($request, $params);
97 | $this->eventDispatcher->dispatch($preSaveEvent);
98 |
99 | if ($preSaveEvent->getResponse()) {
100 | return $preSaveEvent->getResponse();
101 | }
102 |
103 | foreach ($params as $path => $value) {
104 | $this->parameterManager->set($path, $value);
105 | }
106 | $this->parameterManager->save();
107 |
108 | $this->eventDispatcher->dispatch(new SaveEvent());
109 |
110 | $postSaveEvent = new PostSaveEvent($request);
111 | $this->eventDispatcher->dispatch($postSaveEvent);
112 |
113 | if ($postSaveEvent->getResponse()) {
114 | return $postSaveEvent->getResponse();
115 | }
116 |
117 | return new RedirectResponse($this->urlGenerator->generate('sherlockode_configuration.parameters'));
118 | }
119 |
120 | return new Response($this->twig->render($this->editFormTemplate, [
121 | 'form' => $form->createView(),
122 | ]));
123 | }
124 |
125 | public function exportAction(): Response
126 | {
127 | $response = new Response($this->exportManager->exportAsString());
128 | $disposition = HeaderUtils::makeDisposition(
129 | HeaderUtils::DISPOSITION_ATTACHMENT,
130 | 'parameters.yaml'
131 | );
132 | $response->headers->set('Content-Disposition', $disposition);
133 |
134 | return $response;
135 | }
136 |
137 | /**
138 | * @throws LoaderError
139 | * @throws RuntimeError
140 | * @throws SyntaxError
141 | */
142 | public function importAction(Request $request): Response
143 | {
144 | $form = $this->formFactory->create(ImportType::class, [], [
145 | 'action' => $this->urlGenerator->generate('sherlockode_configuration.import'),
146 | 'method' => 'POST',
147 | ]);
148 | $form->handleRequest($this->requestStack->getMainRequest());
149 |
150 | if ($form->isSubmitted() && $form->isValid()) {
151 | $session = $request->hasSession() ? $request->getSession() : null;
152 |
153 | try {
154 | $this->importManager->import($form->get('file')->getData());
155 |
156 | if ($session) {
157 | $session->getFlashbag()->add('success', 'successfully_imported');
158 | }
159 | } catch (\Exception $exception) {
160 | if ($session) {
161 | $session->getFlashbag()->add('error', 'an_error_occurred_during_import');
162 | }
163 | }
164 |
165 | return new RedirectResponse($this->urlGenerator->generate($this->redirectAfterImportRoute));
166 | }
167 |
168 | return new Response($this->twig->render($this->importFormTemplate, ['form' => $form->createView()]));
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/FieldTypePass.php:
--------------------------------------------------------------------------------
1 | removeDefinition('sherlockode_configuration.field.entity');
15 | }
16 |
17 | $definition = $container->findDefinition('sherlockode_configuration.field_manager');
18 | $taggedServices = $container->findTaggedServiceIds('sherlockode_configuration.field');
19 |
20 | foreach ($taggedServices as $id => $tags) {
21 | $definition->addMethodCall('addFieldType', [new Reference($id)]);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
14 |
15 | $rootNode
16 | ->fixXmlConfig('parameter')
17 | ->children()
18 | ->scalarNode('translation_domain')->defaultFalse()->end()
19 | ->arrayNode('entity_class')
20 | ->isRequired()
21 | ->children()
22 | ->scalarNode('parameter')->isRequired()->end()
23 | ->end()
24 | ->end()
25 | ->arrayNode('templates')
26 | ->addDefaultsIfNotSet()
27 | ->children()
28 | ->scalarNode('edit_form')
29 | ->cannotBeEmpty()
30 | ->defaultValue('@SherlockodeConfiguration/parameters.html.twig')
31 | ->end()
32 | ->scalarNode('import_form')
33 | ->cannotBeEmpty()
34 | ->defaultValue('@SherlockodeConfiguration/import.html.twig')
35 | ->end()
36 | ->end()
37 | ->end()
38 | ->arrayNode('parameters')
39 | ->useAttributeAsKey('path')
40 | ->arrayPrototype()
41 | ->children()
42 | ->scalarNode('label')->isRequired()->end()
43 | ->scalarNode('type')->isRequired()->end()
44 | ->scalarNode('default_value')->defaultNull()->end()
45 | ->scalarNode('translation_domain')->defaultNull()->end()
46 | ->variableNode('options')->end()
47 | ->end()
48 | ->end()
49 | ->end()
50 | ->arrayNode('upload')
51 | ->children()
52 | ->scalarNode('directory')
53 | ->cannotBeEmpty()
54 | ->end()
55 | ->scalarNode('uri_prefix')
56 | ->cannotBeEmpty()
57 | ->end()
58 | ->end()
59 | ->end()
60 | ->arrayNode('import')
61 | ->addDefaultsIfNotSet()
62 | ->children()
63 | ->scalarNode('redirect_after')
64 | ->defaultValue('sherlockode_configuration.parameters')
65 | ->cannotBeEmpty()
66 | ->end()
67 | ->end()
68 | ->end()
69 | ->end()
70 | ;
71 |
72 | return $treeBuilder;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/DependencyInjection/SherlockodeConfigurationExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
17 |
18 | $container->setParameter('sherlockode_configuration.parameter_class', $config['entity_class']['parameter']);
19 | $container->setParameter('sherlockode_configuration.parameters', $config['parameters']);
20 | $container->setParameter('sherlockode_configuration.translation_domain', $config['translation_domain']);
21 | $container->setParameter('sherlockode_configuration.templates.edit_form', $config['templates']['edit_form']);
22 | $container->setParameter('sherlockode_configuration.templates.import_form', $config['templates']['import_form']);
23 | $container->setParameter('sherlockode_configuration.import.redirect_after', $config['import']['redirect_after']);
24 |
25 | $fileLocator = new FileLocator(__DIR__ . '/../Resources/config');
26 | $loader = new XmlFileLoader($container, $fileLocator);
27 | $loader->load('services.xml');
28 | $loader->load('field_types.xml');
29 |
30 | $container->registerForAutoconfiguration(FieldTypeInterface::class)
31 | ->addTag('sherlockode_configuration.field');
32 |
33 | $targetDir = $config['upload']['directory'] ?? sys_get_temp_dir();
34 | $webPath = $config['upload']['uri_prefix'] ?? '/';
35 |
36 | $uploadManager = $container->getDefinition('sherlockode_configuration.upload_manager');
37 | if ($uploadManager->getClass() == 'Sherlockode\\ConfigurationBundle\\Manager\\UploadManager') {
38 | $uploadManager->setArguments([
39 | $targetDir,
40 | $webPath,
41 | ]);
42 | }
43 |
44 | $this->registerFormTheme($container);
45 | }
46 |
47 | private function registerFormTheme(ContainerBuilder $container): void
48 | {
49 | $resources = $container->hasParameter('twig.form.resources') ?
50 | $container->getParameter('twig.form.resources') : [];
51 |
52 | \array_unshift($resources, '@SherlockodeConfiguration/form.html.twig');
53 | $container->setParameter('twig.form.resources', $resources);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Event/PostSaveEvent.php:
--------------------------------------------------------------------------------
1 | request = $request;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Event/PreSaveEvent.php:
--------------------------------------------------------------------------------
1 | request = $request;
17 | $this->parameters = $parameters;
18 | }
19 |
20 | public function getParameters(): array
21 | {
22 | return $this->parameters;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Event/RequestEventTrait.php:
--------------------------------------------------------------------------------
1 | request;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Event/ResponseEventTrait.php:
--------------------------------------------------------------------------------
1 | response;
14 | }
15 |
16 | public function setResponse(?Response $response): self
17 | {
18 | $this->response = $response;
19 |
20 | return $this;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Event/SaveEvent.php:
--------------------------------------------------------------------------------
1 | $definition->getOption('required', false),
21 | ];
22 | }
23 |
24 | public function getName(): string
25 | {
26 | return 'checkbox';
27 | }
28 |
29 | public function getModelTransformer(ParameterDefinition $definition): ?TransformerInterface
30 | {
31 | return new BooleanTransformer();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/FieldType/ChoiceField.php:
--------------------------------------------------------------------------------
1 | $definition->getOption('choices', []),
21 | 'multiple' => $definition->getOption('multiple', false),
22 | 'expanded' => $definition->getOption('expanded', false),
23 | 'placeholder' => $definition->getOption('placeholder', null),
24 | ];
25 | }
26 |
27 | public function getName(): string
28 | {
29 | return 'choice';
30 | }
31 |
32 | public function getModelTransformer(ParameterDefinition $definition): ?TransformerInterface
33 | {
34 | if ($definition->getOption('multiple', false)) {
35 | return new ArrayTransformer();
36 | }
37 |
38 | return null;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FieldType/DateTimeField.php:
--------------------------------------------------------------------------------
1 | format(\DateTime::ATOM);
35 | }
36 |
37 | return '';
38 | }
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/FieldType/EntityField.php:
--------------------------------------------------------------------------------
1 | em = $em;
18 | }
19 |
20 | public function getFormType(ParameterDefinition $definition): string
21 | {
22 | return EntityType::class;
23 | }
24 |
25 | public function getFormOptions(ParameterDefinition $definition): array
26 | {
27 | $options = [
28 | 'class' => $definition->getOption('class'),
29 | 'placeholder' => $definition->getOption('placeholder', null),
30 | 'multiple' => $definition->getOption('multiple', null),
31 | ];
32 |
33 | $label = $definition->getOption('choice_label', null);
34 | // only set the choice_label if defined to keep the Form native behavior
35 | if ($label) {
36 | $options['choice_label'] = $label;
37 | }
38 |
39 | return $options;
40 | }
41 |
42 | public function getName(): string
43 | {
44 | return 'entity';
45 | }
46 |
47 | public function getModelTransformer(ParameterDefinition $definition): ?TransformerInterface
48 | {
49 | $class = $definition->getOption('class');
50 | $multiple = $definition->getOption('multiple');
51 |
52 | return new CallbackTransformer(function ($data) use ($class, $multiple) {
53 | if (!$data) {
54 | return null;
55 | }
56 |
57 | if ($multiple) {
58 | $data = explode(',', $data);
59 | $metadata = $this->em->getClassMetadata($class);
60 | $idField = $metadata->getSingleIdentifierFieldName();
61 | $result = $this->em->getRepository($class)->findBy([$idField => $data]);
62 | } else {
63 | $result = $this->em->getRepository($class)->find($data);
64 | }
65 |
66 | return $result;
67 | }, function ($data) use ($class, $multiple) {
68 | if (!$data) {
69 | return null;
70 | }
71 | $metadata = $this->em->getClassMetadata($class);
72 |
73 | if ($multiple) {
74 | $result = [];
75 | foreach ($data as $entity) {
76 | $id = $metadata->getIdentifierValues($entity);
77 | $result[] = reset($id);
78 | }
79 | $result = implode(',', $result);
80 | } else {
81 | $id = $metadata->getIdentifierValues($data);
82 | $result = reset($id);
83 | }
84 |
85 | return $result;
86 | });
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/FieldType/FieldTypeInterface.php:
--------------------------------------------------------------------------------
1 | em = $em;
20 | $this->parameterClass = $parameterClass;
21 | }
22 |
23 | public function getFormType(ParameterDefinition $definition): string
24 | {
25 | return PasswordType::class;
26 | }
27 |
28 | public function getName(): string
29 | {
30 | return 'password';
31 | }
32 |
33 | public function getModelTransformer(ParameterDefinition $definition): ?TransformerInterface
34 | {
35 | $parameter = $this->em->getRepository($this->parameterClass)->findOneBy(['path' => $definition->getPath()]);
36 | $currentValue = $parameter ? $parameter->getValue() : null;
37 |
38 | return new CallbackTransformer(function ($data) {
39 | return $data;
40 | }, function ($data) use ($currentValue) {
41 | if (!$data && $currentValue) {
42 | return $currentValue;
43 | }
44 |
45 | return $data;
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/FieldType/SimpleField.php:
--------------------------------------------------------------------------------
1 | getOption('subtype', TextType::class);
13 |
14 | if (!class_exists($formType)) {
15 | throw new \RuntimeException(sprintf('Undefined form type %s', $formType));
16 | }
17 |
18 | return $formType;
19 | }
20 |
21 | public function getName(): string
22 | {
23 | return 'simple';
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Form/Type/ImageType.php:
--------------------------------------------------------------------------------
1 | uploadManager = $uploadManager;
24 | }
25 |
26 | public function buildForm(FormBuilderInterface $builder, array $options): void
27 | {
28 | $builder
29 | ->add('file', FileType::class, [
30 | 'label' => 'field_type.image.file',
31 | 'required' => false,
32 | 'translation_domain' => 'SherlockodeConfigurationBundle',
33 | ])
34 | ->add('delete', CheckboxType::class, [
35 | 'required' => false,
36 | 'label' => 'field_type.image.delete',
37 | 'translation_domain' => 'SherlockodeConfigurationBundle',
38 | ])
39 | ;
40 |
41 | $filename = '';
42 | $builder->addEventListener(
43 | FormEvents::PRE_SET_DATA,
44 | function (FormEvent $event) use (&$filename) {
45 | $data = $event->getData();
46 | if (isset($data['src'])) {
47 | $filename = $data['src'];
48 | }
49 | }
50 | );
51 |
52 | $builder->addEventListener(
53 | FormEvents::SUBMIT,
54 | function (FormEvent $event) use ($options, &$filename) {
55 | $data = $event->getData();
56 | if ($data['delete']) {
57 | $data['src'] = '';
58 | $this->uploadManager->remove($filename);
59 | }
60 | if ($data['file'] instanceof UploadedFile) {
61 | if ($filename !== '') {
62 | $this->uploadManager->remove($filename);
63 | }
64 | $data['src'] = $this->uploadManager->upload($data['file']);
65 | }
66 | unset($data['file']);
67 | unset($data['delete']);
68 | $event->setData($data);
69 | }
70 | );
71 | }
72 |
73 | public function configureOptions(OptionsResolver $resolver): void
74 | {
75 | $resolver->setDefault('translation_domain', 'SherlockodeConfigurationBundle');
76 | }
77 |
78 | /**
79 | * {@inheritDoc}
80 | */
81 | public function buildView(FormView $view, FormInterface $form, array $options): void
82 | {
83 | $source = '';
84 | $data = $form->getData();
85 | if (!empty($data['src'])) {
86 | $source = $this->uploadManager->resolveUri($data['src']);
87 | }
88 |
89 | $view->vars['source'] = $source;
90 |
91 | }
92 |
93 | public function getBlockPrefix(): string
94 | {
95 | return 'config_image';
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Form/Type/ImportType.php:
--------------------------------------------------------------------------------
1 | add('file', FileType::class, [
17 | 'label' => 'import_file',
18 | 'translation_domain' => 'SherlockodeConfigurationBundle',
19 | 'constraints' => [new NotBlank(), new File(['mimeTypes' => ['text/plain']])],
20 | ])
21 | ;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Form/Type/ParametersType.php:
--------------------------------------------------------------------------------
1 | fieldTypeManager = $fieldTypeManager;
28 | $this->configurationManager = $configurationManager;
29 | $this->constraintManager = $constraintManager;
30 | }
31 |
32 | public function buildForm(FormBuilderInterface $builder, array $options): void
33 | {
34 | foreach ($this->configurationManager->getDefinedParameters() as $definition) {
35 | $field = $this->fieldTypeManager->getField($definition->getType());
36 |
37 | $baseOptions = [
38 | 'label' => $definition->getLabel(),
39 | 'required' => $definition->getOption('required', true),
40 | 'attr' => $definition->getOption('attr', []),
41 | 'row_attr' => $definition->getOption('row_attr', []),
42 | 'label_attr' => $definition->getOption('label_attr', []),
43 | 'help' => $definition->getOption('help'),
44 | 'translation_domain' => $definition->getTranslationDomain(),
45 | 'constraints' => $this->constraintManager->getConstraints($definition->getOption('constraints', [])),
46 | ];
47 | $childOptions = array_merge($baseOptions, $field->getFormOptions($definition));
48 |
49 | $builder
50 | ->add($definition->getPath(), $field->getFormType($definition), $childOptions)
51 | ;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018-2023 Sherlockode
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Manager/ConfigurationManager.php:
--------------------------------------------------------------------------------
1 | definitions = $this->processConfiguration($config, $translationDomain);
17 | }
18 |
19 | /**
20 | * Get the list of parameters in the configuration
21 | *
22 | * @return ParameterDefinition[]
23 | */
24 | public function getDefinedParameters(): array
25 | {
26 | return $this->definitions;
27 | }
28 |
29 | /**
30 | * @throws \Exception
31 | */
32 | public function get(string $path): ParameterDefinition
33 | {
34 | if (!isset($this->definitions[$path])) {
35 | throw new \Exception(sprintf('Undefined parameter path "%s"', $path));
36 | }
37 |
38 | return $this->definitions[$path];
39 | }
40 |
41 | public function has(string $path): bool
42 | {
43 | return isset($this->definitions[$path]);
44 | }
45 |
46 | /**
47 | * @return ParameterDefinition[]
48 | */
49 | private function processConfiguration(array $config, string|false $translationDomain = false): array
50 | {
51 | $result = [];
52 | foreach ($config as $path => $data) {
53 | if (!isset($data['type'])) {
54 | throw new \LogicException(sprintf('No type has been set for parameter "%s"', $path));
55 | }
56 | $definition = new ParameterDefinition($path, $data['type']);
57 | if (isset($data['label'])) {
58 | $definition->setLabel($data['label']);
59 | }
60 | if (!isset($data['translation_domain'])) {
61 | $data['translation_domain'] = $translationDomain;
62 | }
63 | if (isset($data['default_value'])) {
64 | $definition->setDefaultValue($data['default_value']);
65 | }
66 | $definition->setTranslationDomain($data['translation_domain']);
67 | if (isset($data['options']) && is_array($data['options'])) {
68 | $definition->setOptions($data['options']);
69 | }
70 | $result[$path] = $definition;
71 | }
72 |
73 | return $result;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Manager/ConfigurationManagerInterface.php:
--------------------------------------------------------------------------------
1 | $options) {
16 | $constraints[] = $this->create($class, $options);
17 | }
18 | }
19 |
20 | return $constraints;
21 | }
22 |
23 | /**
24 | * @throws \Exception
25 | */
26 | private function create(string $fqcn, ?array $args): Constraint
27 | {
28 | if (!class_exists($fqcn)) {
29 | $fqcn = sprintf('%s%s', AbstractLoader::DEFAULT_NAMESPACE, $fqcn);
30 | }
31 |
32 | if (!is_a($fqcn, Constraint::class, true)) {
33 | throw new \Exception(sprintf('"%s" is not a valid Constraint class.', $fqcn));
34 | }
35 |
36 | if (is_iterable($args)) {
37 | return new $fqcn(...$args);
38 | }
39 |
40 | return new $fqcn();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Manager/ConstraintManagerInterface.php:
--------------------------------------------------------------------------------
1 | parameterManager = $parameterManager;
19 | $this->parameterConverter = $parameterConverter;
20 | $this->vault = $vault;
21 | }
22 |
23 | public function exportAsString(): string
24 | {
25 | $parameters = [];
26 | foreach ($this->parameterManager->getAll() as $path => $value) {
27 | $stringValue = $this->parameterConverter->getStringValue($path, $value);
28 |
29 | if (null !== $stringValue) {
30 | $parameters[$path] = $stringValue;
31 | }
32 | }
33 |
34 | return Yaml::dump($parameters);
35 | }
36 |
37 | /**
38 | * @throws \Exception
39 | */
40 | public function exportInVault(): void
41 | {
42 | foreach ($this->parameterManager->getAll() as $path => $value) {
43 | $stringValue = $this->parameterConverter->getStringValue($path, $value);
44 |
45 | if (null !== $stringValue) {
46 | $this->vault->seal($path, $stringValue);
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Manager/ExportManagerInterface.php:
--------------------------------------------------------------------------------
1 | fieldTypes = [];
17 | }
18 |
19 | public function addFieldType(FieldTypeInterface $fieldType): self
20 | {
21 | $this->fieldTypes[$fieldType->getName()] = $fieldType;
22 |
23 | return $this;
24 | }
25 |
26 | /**
27 | * @throws \Exception
28 | */
29 | public function getField(string $type): FieldTypeInterface
30 | {
31 | if (!isset($this->fieldTypes[$type])) {
32 | throw new \Exception(sprintf('Unknown parameter type "%s"', $type));
33 | }
34 |
35 | return $this->fieldTypes[$type];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Manager/FieldTypeManagerInterface.php:
--------------------------------------------------------------------------------
1 | parameterManager = $parameterManager;
22 | $this->parameterConverter = $parameterConverter;
23 | $this->configurationManager = $configurationManager;
24 | $this->vault = $vault;
25 | }
26 |
27 | public function import(File $source): void
28 | {
29 | $raw = Yaml::parseFile($source->getRealPath());
30 |
31 | foreach ($raw as $path => $stringValue) {
32 | if ($this->configurationManager->has($path)) {
33 | $this->parameterManager->set($path, $this->parameterConverter->getUserValue($path, $stringValue));
34 | }
35 | }
36 |
37 | $this->parameterManager->save();
38 | unlink($source->getRealPath());
39 | }
40 |
41 | public function importFromVault(): void
42 | {
43 | foreach ($this->configurationManager->getDefinedParameters() as $definition) {
44 | $stringValue = $this->vault->reveal($definition->getPath());
45 |
46 | if (null !== $stringValue) {
47 | $this->parameterManager->set($definition->getPath(), $this->parameterConverter->getUserValue($definition->getPath(), $stringValue));
48 | }
49 | }
50 |
51 | $this->parameterManager->save();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Manager/ImportManagerInterface.php:
--------------------------------------------------------------------------------
1 | configurationManager->get($path);
16 | $fieldType = $this->fieldTypeManager->getField($parameterConfig->getType());
17 |
18 | if ($transformer = $fieldType->getModelTransformer($parameterConfig)) {
19 | $value = $transformer->reverseTransform($value);
20 | }
21 |
22 | return $value;
23 | }
24 |
25 | public function getUserValue(string $path, ?string $value): mixed
26 | {
27 | $parameterDefinition = $this->configurationManager->get($path);
28 | $fieldType = $this->fieldTypeManager->getField($parameterDefinition->getType());
29 |
30 | if ($transformer = $fieldType->getModelTransformer($parameterDefinition)) {
31 | $value = $transformer->transform($value);
32 | }
33 |
34 | return $value;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Manager/ParameterConverterInterface.php:
--------------------------------------------------------------------------------
1 | value
38 | */
39 | private array $data;
40 |
41 | private bool $loaded;
42 |
43 | private ConfigurationManagerInterface $configurationManager;
44 |
45 | private ParameterConverterInterface $parameterConverter;
46 |
47 | /**
48 | * ParameterManager constructor.
49 | */
50 | public function __construct(
51 | EntityManagerInterface $em,
52 | string $class,
53 | ConfigurationManagerInterface $configurationManager,
54 | ParameterConverterInterface $parameterConverter
55 | ) {
56 | $this->em = $em;
57 | $this->class = $class;
58 | $this->configurationManager = $configurationManager;
59 | $this->parameters = [];
60 | $this->newParameters = [];
61 | $this->data = [];
62 | $this->loaded = false;
63 | $this->parameterConverter = $parameterConverter;
64 | }
65 |
66 | public function getClass(): string
67 | {
68 | return $this->class;
69 | }
70 |
71 | public function getAll(): array
72 | {
73 | if (false === $this->loaded) {
74 | $this->loadParameters();
75 | }
76 |
77 | foreach ($this->configurationManager->getDefinedParameters() as $path => $parameterDefinition) {
78 | if (!isset($this->data[$path])) {
79 | // check if the value exists in the DB
80 | if (isset($this->parameters[$path])) {
81 | $value = $this->parameters[$path]->getValue();
82 | } else {
83 | $value = $parameterDefinition->getDefaultValue();
84 | }
85 |
86 | // check that every parameter has been transformed to its user value
87 | if (!is_null($value)) {
88 | $this->data[$path] = $this->parameterConverter->getUserValue($path, $value);
89 | }
90 | }
91 | }
92 |
93 | return $this->data;
94 | }
95 |
96 | public function get(string $path, mixed $default = null): mixed
97 | {
98 | if (false === $this->loaded) {
99 | $this->loadParameters();
100 | }
101 |
102 | if (isset($this->data[$path])) {
103 | return $this->data[$path];
104 | }
105 |
106 | $value = null;
107 | if (isset($this->parameters[$path])) {
108 | $value = $this->parameters[$path]->getValue();
109 | } else {
110 | $value = $this->configurationManager->get($path)->getDefaultValue();
111 | }
112 |
113 | // transform to user value only when the value is requested
114 | if (!is_null($value)) {
115 | $this->data[$path] = $this->parameterConverter->getUserValue($path, $value);
116 | return $this->data[$path];
117 | }
118 |
119 | return $value ?? $default;
120 | }
121 |
122 | public function set(string $path, mixed $value): self
123 | {
124 | if (false === $this->loaded) {
125 | $this->loadParameters();
126 | }
127 | if (!$this->configurationManager->has($path)) {
128 | return $this;
129 | }
130 | if (!isset($this->parameters[$path])) {
131 | /** @var ParameterInterface $parameter */
132 | $parameter = new $this->class();
133 | $parameter->setPath($path);
134 | $this->parameters[$path] = $parameter;
135 | $this->newParameters[] = $parameter;
136 | }
137 | $stringValue = $this->parameterConverter->getStringValue($path, $value);
138 | $this->parameters[$path]->setValue($stringValue);
139 | $this->data[$path] = $this->parameterConverter->getUserValue($path, $stringValue);
140 |
141 | return $this;
142 | }
143 |
144 | /**
145 | * Save all parameters into the database
146 | */
147 | public function save(): void
148 | {
149 | foreach ($this->newParameters as $parameter) {
150 | $this->em->persist($parameter);
151 | }
152 | $this->em->flush($this->parameters);
153 | }
154 |
155 | /**
156 | * Load the parameters from the database
157 | * and transform their string value into the expected type
158 | */
159 | private function loadParameters(): void
160 | {
161 | $parameters = $this->em->getRepository($this->class)->findAll();
162 | /** @var ParameterInterface $parameter */
163 | foreach ($parameters as $parameter) {
164 | if (!$this->configurationManager->has($parameter->getPath())) {
165 | continue;
166 | }
167 | $this->parameters[$parameter->getPath()] = $parameter;
168 | }
169 | $this->loaded = true;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/Manager/ParameterManagerInterface.php:
--------------------------------------------------------------------------------
1 | value
11 | */
12 | public function getAll(): array;
13 |
14 | /**
15 | * Save all parameters into the database
16 | */
17 | public function save(): void;
18 |
19 | /**
20 | * Get an existing parameter value for a given path
21 | */
22 | public function get(string $path, mixed $default = null): mixed;
23 |
24 | /**
25 | * Set the value of a parameter at given path
26 | */
27 | public function set(string $path, mixed $value): self;
28 | }
29 |
--------------------------------------------------------------------------------
/Manager/UploadManager.php:
--------------------------------------------------------------------------------
1 | targetDir = $targetDir;
16 | $this->webPath = $webPath;
17 | }
18 |
19 | /**
20 | * Upload file on server
21 | */
22 | public function upload(?UploadedFile $file = null): string
23 | {
24 | if ($file === null) {
25 | return '';
26 | }
27 |
28 | $filename = $this->generateFilename($file);
29 | $file->move($this->targetDir, $filename);
30 |
31 | return $filename;
32 | }
33 |
34 | /**
35 | * Remove file
36 | */
37 | public function remove(string $filename): void
38 | {
39 | $filepath = $this->getFilePath($filename);
40 |
41 | if (!file_exists($filepath)) {
42 | return;
43 | }
44 |
45 | unlink($filepath);
46 | }
47 |
48 | public function getFilePath(string $filename): string
49 | {
50 | return $this->targetDir . DIRECTORY_SEPARATOR . $filename;
51 | }
52 |
53 | /**
54 | * Generate the filename to store on disk
55 | */
56 | private function generateFilename(UploadedFile $file): string
57 | {
58 | $extension = $file->getClientOriginalExtension();
59 | $filename = str_replace('.' . $extension, '', $file->getClientOriginalName());
60 |
61 | $filename = $filename . '_' . md5(uniqid()) . '.' . $extension;
62 |
63 | return $filename;
64 | }
65 |
66 | public function resolveUri(string $filename): string
67 | {
68 | return $this->webPath . '/' . $filename;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Manager/UploadManagerInterface.php:
--------------------------------------------------------------------------------
1 | id;
16 | }
17 |
18 | public function getPath(): string
19 | {
20 | return $this->path;
21 | }
22 |
23 | public function setPath(string $path): self
24 | {
25 | $this->path = $path;
26 |
27 | return $this;
28 | }
29 |
30 | public function getValue(): ?string
31 | {
32 | return $this->value;
33 | }
34 |
35 | public function setValue(?string $value): self
36 | {
37 | $this->value = $value;
38 |
39 | return $this;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Model/ParameterInterface.php:
--------------------------------------------------------------------------------
1 | path = $path;
25 | $this->type = $type;
26 |
27 | $this->label = $this->path;
28 | }
29 |
30 | public function getPath(): string
31 | {
32 | return $this->path;
33 | }
34 |
35 | public function getLabel(): string
36 | {
37 | return $this->label;
38 | }
39 |
40 | public function setLabel(string $label): self
41 | {
42 | $this->label = $label;
43 |
44 | return $this;
45 | }
46 |
47 | public function getType(): string
48 | {
49 | return $this->type;
50 | }
51 |
52 | public function getDefaultValue(): ?string
53 | {
54 | return $this->defaultValue;
55 | }
56 |
57 | public function setDefaultValue(?string $defaultValue): self
58 | {
59 | $this->defaultValue = $defaultValue;
60 |
61 | return $this;
62 | }
63 |
64 | public function getTranslationDomain(): string
65 | {
66 | return $this->translationDomain;
67 | }
68 |
69 | public function setTranslationDomain(string $translationDomain): self
70 | {
71 | $this->translationDomain = $translationDomain;
72 |
73 | return $this;
74 | }
75 |
76 | public function getOption(string $optionName, mixed $default = null): mixed
77 | {
78 | if (!isset($this->options[$optionName])) {
79 | return $default;
80 | }
81 |
82 | return $this->options[$optionName];
83 | }
84 |
85 | public function getOptions(): array
86 | {
87 | return $this->options;
88 | }
89 |
90 | public function setOptions(array $options): self
91 | {
92 | $this->options = $options;
93 |
94 | return $this;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Sherlockode ConfigurationBundle
2 | ===============================
3 |
4 | This bundle is built to allow users to define configuration options, useful in admin panels.
5 | Get rid of the business logic variables in your `ENV` or `parameters.yml` file !
6 |
7 | Available features:
8 |
9 | * Define multiple configuration entries available for the users
10 | * Retrieve configuration values anywhere in your code for business logic
11 | * Use or create custom parameter types for a better user experience
12 |
13 | [](https://circleci.com/gh/sherlockode/configuration-bundle)
14 | [](https://packagist.org/packages/sherlockode/configuration-bundle)
15 | [](https://packagist.org/packages/sherlockode/configuration-bundle)
16 |
17 | ## Installation
18 |
19 | The best way to install this bundle is to rely on [Composer](https://getcomposer.org/):
20 |
21 | ```bash
22 | $ composer require sherlockode/configuration-bundle
23 | ```
24 |
25 | ## Setup
26 |
27 | Enable the bundle in the kernel
28 |
29 | ```php
30 | ['all' => true],
36 | ];
37 | ```
38 |
39 | You will need a `Parameter` entity in order to store the configuration values in the database.
40 |
41 | ```php
42 | getAll();
204 | // or using an associative array:
205 | // $data = ['contact_email' => 'me@example.com', 'max_user_login_attempts' => 5];
206 |
207 | $form = $this->createForm(ParametersType::class, $data);
208 | // handle form submission
209 | $form->handleRequest($request);
210 | if ($form->isSubmitted() && $form->isValid()) {
211 | $params = $form->getData();
212 | foreach ($params as $path => $value) {
213 | $parameterManager->set($path, $value);
214 | }
215 | $parameterManager->save();
216 | }
217 | //...
218 | ```
219 |
220 | ### Access configuration values
221 |
222 | You are now able to retrieve any configuration value by using the `get` method from the parameter manager.
223 | It is possible to provide a default value to return if the entry has not been set yet.
224 |
225 | ```php
226 | $email = $parameterManager->get('contact_email');
227 | $maxAttempts = $parameterManager->get('max_user_login_attempts', 5);
228 | ```
229 |
230 | ### Import / export from the browser
231 |
232 | You can export or import parameters in a yaml file. You have two routes for these operations:
233 |
234 | * `sherlockode_configuration.export`
235 | * `sherlockode_configuration.import`
236 |
237 | You also can customize the import form template by defining your own in the configuration:
238 |
239 | ```yaml
240 | # config/packages/sherlockode_configuration.yaml
241 | sherlockode_configuration:
242 | templates:
243 | import_form: 'Parameter/my_import_form.html.twig'
244 | ```
245 |
246 | ### Import / export from CLI
247 |
248 | You can export or import your parameters with these two Symfony commands:
249 |
250 | * `sherlockode:configuration:export`
251 | * `sherlockode:configuration:import`
252 |
253 | These commands rely on the [Symfony secrets mechanism](https://symfony.com/doc/current/configuration/secrets.html).
254 | So be sure to generate your credentials with the following command:
255 |
256 | ```shell
257 | $ php bin/console secrets:generate-keys
258 | ```
259 |
260 | ### Events
261 |
262 | When parameters are saved in database, multiple events are dispatched
263 |
264 | * `Sherlockode\ConfigurationBundle\Event\PreSaveEvent` is dispatch before saving parameters;
265 | * `Sherlockode\ConfigurationBundle\Event\SaveEvent` is dispatch just after saving new parameters values
266 | in database;
267 | * `Sherlockode\ConfigurationBundle\Event\PostSaveEvent` is dispatch just after the SaveEvent. It can be
268 | useful for customizing redirection or adding flash message for example;
269 |
270 | ## Field types
271 |
272 | ### Default types
273 |
274 | Here are the field types provided in the bundle, located in the namespace `Sherlockode\ConfigurationBundle\FieldType` :
275 |
276 | * simple
277 | * checkbox
278 | * choice
279 | * datetime
280 | * entity
281 | * image
282 | * password
283 |
284 | ### Custom Field types
285 |
286 | In order to add custom field types, you should create a service implementing the `FieldTypeInterface` interface
287 | and tag it with `sherlockode_configuration.field` (or use autoconfiguration).
288 |
289 | The `getName()` return value is the alias of the field type to use in the configuration (like `simple` or `choice`).
290 |
291 | ### Using transformers
292 |
293 | Due to the format of the Parameter entity in the database (the value is stored as a string, whatever the parameter type),
294 | complex values cannot be stored directly.
295 | For instance, we can serialize an array to fit the string type, or we may store the ID of a database entity.
296 | The process may vary depending on your needs and the value to store, but the application needs to be aware of the process
297 | to transform the PHP data into a string and the opposite process. This is done through transformers.
298 |
299 | A transformer is an object implementing the `Sherlockode\ConfigurationBundle\Transformer\TransformerInterface`.
300 | The interface has two methods `transform` and `reverseTransform`, similarly to the transformers used by Symfony in the Form Component.
301 |
302 | The `transform` method takes the string representation and returns the PHP value,
303 | when the `reverseTransform` takes your PHP value and returns back the corresponding scalar value.
304 |
305 | In order to be used, an instance of the transformer should be returned by the `getModelTransformer`
306 | method of the corresponding field type. If this method returns `null`, the bundle considers that no transformation is needed.
307 |
308 | The bundle also provides a `CallbackTransformer` that can be used for faster implementations.
309 | For instance handling an array can be done like this :
310 |
311 | ```php
312 | public function getModelTransformer(ParameterDefinition $definition): ?TransformerInterface
313 | {
314 | return new CallbackTransformer(
315 | function ($data) {
316 | if (!$data) {
317 | return null;
318 | }
319 | if (false !== ($unserialized = @unserialize($data))) {
320 | return $unserialized;
321 | }
322 | return $data;
323 | },
324 | function ($data) {
325 | if (is_array($data)) {
326 | return serialize($data);
327 | }
328 | return $data;
329 | }
330 | );
331 | }
332 | ```
333 |
334 | ## License
335 |
336 | This bundle is under the MIT license. Check the details in the [dedicated file](LICENSE)
337 |
--------------------------------------------------------------------------------
/Resources/config/field_types.xml:
--------------------------------------------------------------------------------
1 |
2 |