├── LICENSE ├── composer.json ├── infection.json.dist ├── rector.php └── src ├── Controller └── Action │ └── ShowTermsAction.php ├── DependencyInjection ├── Configuration.php └── SetonoSyliusTermsExtension.php ├── EventSubscriber └── AddMenuSubscriber.php ├── Fixture ├── Factory │ └── TermsExampleFactory.php └── TermsFixture.php ├── Form ├── Extension │ └── FormTypeExtension.php └── Type │ ├── TermsCheckboxCollectionType.php │ ├── TermsTranslationType.php │ └── TermsType.php ├── Model ├── Terms.php ├── TermsInterface.php ├── TermsTranslation.php └── TermsTranslationInterface.php ├── Provider ├── TermsProvider.php └── TermsProviderInterface.php ├── Renderer ├── LabelRenderer.php └── LabelRendererInterface.php ├── Repository ├── TermsRepository.php └── TermsRepositoryInterface.php ├── Resources ├── config │ ├── app │ │ └── fixtures.yaml │ ├── doctrine │ │ └── model │ │ │ ├── Terms.orm.xml │ │ │ └── TermsTranslation.orm.xml │ ├── routes.yaml │ ├── routes │ │ ├── admin.yaml │ │ └── shop.yaml │ ├── routes_no_locale.yaml │ ├── services.xml │ ├── services │ │ ├── controller.xml │ │ ├── event_subscriber.xml │ │ ├── fixture.xml │ │ ├── form.xml │ │ ├── provider.xml │ │ ├── renderer.xml │ │ └── twig.xml │ └── validation │ │ ├── Terms.xml │ │ └── TermsTranslation.xml ├── public │ ├── slugger.js │ └── slugify-terms-name.js ├── translations │ ├── messages.da.yaml │ ├── messages.de.yaml │ ├── messages.en.yaml │ ├── messages.fr.yaml │ ├── messages.nl.yaml │ ├── validators.da.yaml │ ├── validators.de.yaml │ ├── validators.en.yaml │ ├── validators.fr.yaml │ └── validators.nl.yaml └── views │ ├── admin │ ├── grid │ │ └── field │ │ │ └── channels.html.twig │ └── terms │ │ ├── _form.html.twig │ │ └── _javascripts.html.twig │ └── shop │ └── terms │ ├── link.html.twig │ ├── show.html.twig │ └── show │ └── _breadcrumb.html.twig ├── SetonoSyliusTermsPlugin.php └── Twig ├── TermsExtension.php └── TermsRuntime.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Setono 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setono/sylius-terms-plugin", 3 | "description": "Sylius terms and conditions plugin", 4 | "license": "MIT", 5 | "type": "sylius-plugin", 6 | "keywords": [ 7 | "sylius", 8 | "sylius-plugin", 9 | "terms" 10 | ], 11 | "require": { 12 | "php": ">=8.1", 13 | "doctrine/collections": "^1.6", 14 | "doctrine/orm": "^2.7", 15 | "fakerphp/faker": "^1.23", 16 | "knplabs/knp-menu": "^3.1", 17 | "sylius/channel": "^1.0", 18 | "sylius/channel-bundle": "^1.0", 19 | "sylius/core": "^1.0", 20 | "sylius/core-bundle": "^1.0", 21 | "sylius/locale": "^1.0", 22 | "sylius/resource-bundle": "^1.6", 23 | "sylius/ui-bundle": "^1.0", 24 | "symfony/config": "^5.4 || ^6.4 || ^7.0", 25 | "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", 26 | "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", 27 | "symfony/form": "^5.4 || ^6.4 || ^7.0", 28 | "symfony/http-foundation": "^5.4 || ^6.4 || ^7.0", 29 | "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0", 30 | "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", 31 | "symfony/routing": "^5.4 || ^6.4 || ^7.0", 32 | "symfony/string": "^5.4 || ^6.4 || ^7.0", 33 | "symfony/translation-contracts": "^1.1 || ^2.4 || ^3.4", 34 | "symfony/validator": "^5.4 || ^6.4 || ^7.0", 35 | "twig/twig": "^2.14 || ^3.8", 36 | "webmozart/assert": "^1.11" 37 | }, 38 | "require-dev": { 39 | "api-platform/core": "^2.7.16", 40 | "babdev/pagerfanta-bundle": "^3.8", 41 | "behat/behat": "^3.14", 42 | "doctrine/doctrine-bundle": "^2.11", 43 | "infection/infection": "^0.27.9", 44 | "jms/serializer-bundle": "^4.2", 45 | "lexik/jwt-authentication-bundle": "^2.17", 46 | "matthiasnoback/symfony-config-test": "^4.3 || ^5.1", 47 | "matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0", 48 | "phpspec/prophecy-phpunit": "^2.1", 49 | "phpunit/phpunit": "^9.6", 50 | "psalm/plugin-phpunit": "^0.18", 51 | "setono/code-quality-pack": "^2.7", 52 | "sylius/sylius": "~1.12.13", 53 | "symfony/debug-bundle": "^5.4 || ^6.4 || ^7.0", 54 | "symfony/dotenv": "^5.4 || ^6.4 || ^7.0", 55 | "symfony/intl": "^5.4 || ^6.4 || ^7.0", 56 | "symfony/property-info": "^5.4 || ^6.4 || ^7.0", 57 | "symfony/serializer": "^5.4 || ^6.4 || ^7.0", 58 | "symfony/web-profiler-bundle": "^5.4 || ^6.4 || ^7.0", 59 | "symfony/webpack-encore-bundle": "^1.17", 60 | "willdurand/negotiation": "^3.1" 61 | }, 62 | "prefer-stable": true, 63 | "autoload": { 64 | "psr-4": { 65 | "Setono\\SyliusTermsPlugin\\": "src/" 66 | } 67 | }, 68 | "autoload-dev": { 69 | "psr-4": { 70 | "Setono\\SyliusTermsPlugin\\Tests\\": "tests/" 71 | }, 72 | "classmap": [ 73 | "tests/Application/Kernel.php" 74 | ] 75 | }, 76 | "config": { 77 | "allow-plugins": { 78 | "dealerdirect/phpcodesniffer-composer-installer": false, 79 | "ergebnis/composer-normalize": true, 80 | "infection/extension-installer": true, 81 | "symfony/thanks": false 82 | }, 83 | "sort-packages": true 84 | }, 85 | "scripts": { 86 | "analyse": "psalm", 87 | "check-style": "ecs check", 88 | "fix-style": "ecs check --fix", 89 | "phpunit": "phpunit" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php://stderr", 9 | "github": true, 10 | "stryker": { 11 | "badge": "2.x" 12 | } 13 | }, 14 | "minMsi": 3.70, 15 | "minCoveredMsi": 100.00 16 | } 17 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | cacheClass(FileCacheStorage::class); 11 | $rectorConfig->cacheDirectory('./.build/rector'); 12 | 13 | $rectorConfig->paths([ 14 | __DIR__ . '/src', 15 | __DIR__ . '/tests', 16 | ]); 17 | 18 | $rectorConfig->skip([ 19 | __DIR__ . '/tests/Application', 20 | ]); 21 | 22 | $rectorConfig->sets([ 23 | LevelSetList::UP_TO_PHP_81 24 | ]); 25 | }; 26 | -------------------------------------------------------------------------------- /src/Controller/Action/ShowTermsAction.php: -------------------------------------------------------------------------------- 1 | termsRepository->findOneByChannelAndLocaleAndSlug( 28 | $this->channelContext->getChannel(), 29 | $this->localeContext->getLocaleCode(), 30 | $slug, 31 | ); 32 | 33 | if (null === $terms) { 34 | throw new NotFoundHttpException('The terms page does not exist'); 35 | } 36 | 37 | try { 38 | // here we test if the user has placed a special template for this particular set of terms 39 | // if not it throws an exception, and we will use the default template 40 | $template = $this->twig->load(sprintf( 41 | '@SetonoSyliusTermsPlugin/shop/terms/show/%s.html.twig', 42 | (string) $terms->getCode(), 43 | )); 44 | } catch (LoaderError) { 45 | $template = $this->twig->load('@SetonoSyliusTermsPlugin/shop/terms/show.html.twig'); 46 | } 47 | 48 | return new Response($template->render([ 49 | 'terms' => $terms, 50 | ])); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 27 | 28 | /** @psalm-suppress UndefinedInterfaceMethod,PossiblyNullReference,MixedMethodCall */ 29 | $rootNode 30 | ->addDefaultsIfNotSet() 31 | ->children() 32 | ->arrayNode('forms') 33 | ->defaultValue([CompleteType::class => ['label' => null]]) 34 | ->useAttributeAsKey('class') 35 | ->arrayPrototype() 36 | ->children() 37 | ->scalarNode('label')->defaultNull()->end() 38 | ->end() 39 | ->end() 40 | ->end() 41 | ->arrayNode('routing') 42 | ->addDefaultsIfNotSet() 43 | ->children() 44 | ->scalarNode('terms') 45 | ->defaultValue('terms') 46 | ->cannotBeEmpty() 47 | ->info('The path prefix for displaying terms on the frontend. Example: https://example.com/terms/privacy-policy - here "terms" is the path prefix') 48 | ; 49 | 50 | $this->addResourcesSection($rootNode); 51 | 52 | return $treeBuilder; 53 | } 54 | 55 | private function addResourcesSection(ArrayNodeDefinition $node): void 56 | { 57 | /** @psalm-suppress UndefinedInterfaceMethod,PossiblyNullReference,MixedMethodCall */ 58 | $node 59 | ->children() 60 | ->arrayNode('resources') 61 | ->addDefaultsIfNotSet() 62 | ->children() 63 | ->arrayNode('terms') 64 | ->addDefaultsIfNotSet() 65 | ->children() 66 | ->variableNode('options')->end() 67 | ->arrayNode('classes') 68 | ->addDefaultsIfNotSet() 69 | ->children() 70 | ->scalarNode('model')->defaultValue(Terms::class)->cannotBeEmpty()->end() 71 | ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() 72 | ->scalarNode('repository')->defaultValue(TermsRepository::class)->cannotBeEmpty()->end() 73 | ->scalarNode('factory')->defaultValue(Factory::class)->end() 74 | ->scalarNode('form')->defaultValue(TermsType::class)->cannotBeEmpty()->end() 75 | ->end() 76 | ->end() 77 | ->arrayNode('translation') 78 | ->addDefaultsIfNotSet() 79 | ->children() 80 | ->variableNode('options')->end() 81 | ->arrayNode('classes') 82 | ->addDefaultsIfNotSet() 83 | ->children() 84 | ->scalarNode('model')->defaultValue(TermsTranslation::class)->cannotBeEmpty()->end() 85 | ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() 86 | ->scalarNode('repository')->cannotBeEmpty()->end() 87 | ->scalarNode('factory')->defaultValue(Factory::class)->end() 88 | ->scalarNode('form')->defaultValue(TermsTranslationType::class)->cannotBeEmpty()->end() 89 | ; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DependencyInjection/SetonoSyliusTermsExtension.php: -------------------------------------------------------------------------------- 1 | , routing: array{terms: string}, resources: array} $config 24 | */ 25 | $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); 26 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 27 | 28 | foreach ($config['forms'] as $form => $formConfig) { 29 | $reflectionClass = new ReflectionClass($form); 30 | $label = $formConfig['label'] ?? sprintf('setono_sylius_terms.form.terms.term_form.%s', u($reflectionClass->getShortName())->snake()->trimSuffix('_type')->toString()); 31 | $config['forms'][$form]['label'] = $label; 32 | } 33 | 34 | $container->setParameter('setono_sylius_terms.forms', $config['forms']); 35 | $container->setParameter('setono_sylius_terms.terms_path', $config['routing']['terms']); 36 | 37 | $loader->load('services.xml'); 38 | 39 | $this->registerResources( 40 | 'setono_sylius_terms', 41 | SyliusResourceBundle::DRIVER_DOCTRINE_ORM, 42 | $config['resources'], 43 | $container, 44 | ); 45 | } 46 | 47 | public function prepend(ContainerBuilder $container): void 48 | { 49 | $container->prependExtensionConfig('sylius_grid', [ 50 | 'grids' => [ 51 | 'setono_sylius_terms_terms' => [ 52 | 'driver' => [ 53 | 'name' => 'doctrine/orm', 54 | 'options' => [ 55 | 'class' => '%setono_sylius_terms.model.terms.class%', 56 | ], 57 | ], 58 | 'fields' => [ 59 | 'code' => [ 60 | 'type' => 'string', 61 | 'label' => 'setono_sylius_terms.ui.code', 62 | ], 63 | 'name' => [ 64 | 'type' => 'string', 65 | 'label' => 'setono_sylius_terms.ui.name', 66 | ], 67 | 'channels' => [ 68 | 'type' => 'twig', 69 | 'label' => 'setono_sylius_terms.ui.channels', 70 | 'options' => [ 71 | 'template' => '@SetonoSyliusTermsPlugin/admin/grid/field/channels.html.twig', 72 | ], 73 | ], 74 | ], 75 | 'actions' => [ 76 | 'main' => [ 77 | 'create' => [ 78 | 'type' => 'create', 79 | ], 80 | ], 81 | 'item' => [ 82 | 'update' => [ 83 | 'type' => 'update', 84 | ], 85 | 'delete' => [ 86 | 'type' => 'delete', 87 | ], 88 | ], 89 | ], 90 | ], 91 | ], 92 | ]); 93 | 94 | $container->prependExtensionConfig('sylius_ui', [ 95 | 'events' => [ 96 | 'setono_sylius_terms.admin.terms.create.javascripts' => [ 97 | 'blocks' => [ 98 | 'javascripts' => [ 99 | 'template' => '@SetonoSyliusTermsPlugin/admin/terms/_javascripts.html.twig', 100 | ], 101 | ], 102 | ], 103 | ], 104 | ]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/EventSubscriber/AddMenuSubscriber.php: -------------------------------------------------------------------------------- 1 | 'add', 17 | ]; 18 | } 19 | 20 | public function add(MenuBuilderEvent $event): void 21 | { 22 | $menu = $event->getMenu(); 23 | 24 | $subMenu = $menu->getChild('configuration'); 25 | 26 | if (null !== $subMenu) { 27 | $this->addChild($subMenu); 28 | } else { 29 | $this->addChild($menu->getFirstChild()); 30 | } 31 | } 32 | 33 | private function addChild(ItemInterface $item): void 34 | { 35 | $item 36 | ->addChild('terms', [ 37 | 'route' => 'setono_sylius_terms_admin_terms_index', 38 | ]) 39 | ->setLabel('setono_sylius_terms.menu.admin.main.configuration.terms') 40 | ->setLabelAttribute('icon', 'check circle outline') 41 | ; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Fixture/Factory/TermsExampleFactory.php: -------------------------------------------------------------------------------- 1 | faker = FakerFactory::create(); 36 | $this->optionsResolver = new OptionsResolver(); 37 | 38 | $this->configureOptions($this->optionsResolver); 39 | } 40 | 41 | public function create(array $options = []): TermsInterface 42 | { 43 | $options = $this->optionsResolver->resolve($options); 44 | 45 | /** @var TermsInterface|null $terms */ 46 | $terms = $this->termsRepository->findOneBy(['code' => $options['code']]); 47 | 48 | if (null === $terms) { 49 | /** @var TermsInterface $terms */ 50 | $terms = $this->termsFactory->createNew(); 51 | } 52 | 53 | $terms->setCode($options['code']); 54 | foreach ($options['channels'] as $channel) { 55 | $terms->addChannel($channel); 56 | } 57 | 58 | $terms->setEnabled($options['enabled']); 59 | $terms->setForms($options['forms']); 60 | 61 | // add translation for each defined locales 62 | foreach ($this->getLocales() as $localeCode) { 63 | $this->createTranslation($terms, $localeCode, $options); 64 | } 65 | 66 | // create or replace with custom translations 67 | foreach ($options['translations'] as $localeCode => $translationOptions) { 68 | $this->createTranslation($terms, $localeCode, $translationOptions); 69 | } 70 | 71 | return $terms; 72 | } 73 | 74 | protected function createTranslation(TermsInterface $terms, string $localeCode, array $options = []): void 75 | { 76 | $options = $this->optionsResolver->resolve($options); 77 | 78 | $terms->setCurrentLocale($localeCode); 79 | $terms->setFallbackLocale($localeCode); 80 | 81 | $terms->setName($options['name']); 82 | $terms->setLabel($options['label']); 83 | $terms->setContent($options['content']); 84 | $terms->setSlug($options['slug'] ?? (new AsciiSlugger())->slug($options['name'])->toString()); 85 | } 86 | 87 | protected function configureOptions(OptionsResolver $resolver): void 88 | { 89 | $resolver 90 | ->setDefault('name', fn (Options $options): string => 91 | /** @psalm-suppress MixedArgumentTypeCoercion */ 92 | implode(' ', (array) $this->faker->words(3))) 93 | 94 | ->setDefault('code', fn (Options $options): string => StringInflector::nameToCode($options['name'])) 95 | 96 | ->setDefault('channels', LazyOption::randomOnes($this->channelRepository, 3)) 97 | ->setAllowedTypes('channels', ['array']) 98 | ->setNormalizer('channels', LazyOption::findBy($this->channelRepository, 'code')) 99 | 100 | ->setDefault('slug', null) 101 | 102 | ->setDefault('enabled', true) 103 | ->setAllowedTypes('enabled', ['bool']) 104 | 105 | ->setDefault('label', function (Options $options): string { 106 | return $this->faker->text(60); // @todo add link to this text 107 | }) 108 | 109 | ->setDefault('content', fn (Options $options): string => $this->faker->paragraph) 110 | 111 | ->setDefault('translations', []) 112 | ->setAllowedTypes('translations', ['array']) 113 | 114 | ->setDefault('forms', [CompleteType::class]) 115 | ->setAllowedTypes('forms', ['array']) 116 | ; 117 | } 118 | 119 | private function getLocales(): iterable 120 | { 121 | /** @var LocaleInterface[] $locales */ 122 | $locales = $this->localeRepository->findAll(); 123 | foreach ($locales as $locale) { 124 | yield $locale->getCode(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Fixture/TermsFixture.php: -------------------------------------------------------------------------------- 1 | children() 22 | ->scalarNode('code')->cannotBeEmpty()->end() 23 | ->scalarNode('name')->cannotBeEmpty()->end() 24 | ->scalarNode('slug')->cannotBeEmpty()->end() 25 | ->scalarNode('label')->cannotBeEmpty()->end() 26 | ->scalarNode('content')->cannotBeEmpty()->end() 27 | ->booleanNode('enabled')->defaultTrue()->end() 28 | ->variableNode('forms')->end() 29 | ->variableNode('translations')->cannotBeEmpty()->defaultValue([])->end() 30 | ->variableNode('channels')->cannotBeEmpty()->defaultValue([])->end() 31 | ; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Form/Extension/FormTypeExtension.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | private array $forms; 25 | 26 | /** 27 | * @param array, array{label: string}> $forms 28 | */ 29 | public function __construct( 30 | private readonly TermsProviderInterface $termsProvider, 31 | array $forms, 32 | ) { 33 | $this->forms = array_keys($forms); 34 | } 35 | 36 | public function buildForm(FormBuilderInterface $builder, array $options): void 37 | { 38 | $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event): void { 39 | $formTypeClass = $event->getForm()->getConfig()->getType()->getInnerType(); 40 | if (!in_array($formTypeClass::class, $this->forms, true)) { 41 | return; 42 | } 43 | 44 | $terms = $this->termsProvider->getTerms($formTypeClass::class); 45 | if ([] === $terms) { 46 | return; 47 | } 48 | 49 | $event->getForm()->add('terms', TermsCheckboxCollectionType::class, [ 50 | 'terms' => $terms, 51 | ]); 52 | }); 53 | } 54 | 55 | public static function getExtendedTypes(): Generator 56 | { 57 | yield FormType::class; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Form/Type/TermsCheckboxCollectionType.php: -------------------------------------------------------------------------------- 1 | addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) use ($options): void { 28 | $form = $event->getForm(); 29 | $parent = $form->getParent(); 30 | 31 | // We need to collect all validation groups from the parent forms, so that our constraints are applied 32 | $validationGroups = []; 33 | while (null !== $parent) { 34 | $groups = $parent->getConfig()->getOption('validation_groups'); 35 | if (!is_array($groups)) { 36 | continue; 37 | } 38 | 39 | $validationGroups[] = $groups; 40 | $parent = $parent->getParent(); 41 | } 42 | 43 | $validationGroups = array_filter(array_merge(...$validationGroups), static fn (mixed $group): bool => is_string($group)); 44 | 45 | /** @var TermsInterface $terms */ 46 | foreach ($options['terms'] as $terms) { 47 | $form->add((string) $terms->getCode(), CheckboxType::class, [ 48 | 'label' => $this->labelRenderer->render($terms), 49 | 'label_html' => true, 50 | 'required' => false, 51 | 'value' => true, 52 | 'mapped' => false, 53 | 'constraints' => [ 54 | new IsTrue([ 55 | 'message' => $options['error_message'], 56 | 'groups' => $validationGroups, 57 | ]), 58 | ], 59 | ]); 60 | } 61 | }); 62 | } 63 | 64 | public function configureOptions(OptionsResolver $resolver): void 65 | { 66 | $resolver 67 | ->setRequired('terms') 68 | ->setAllowedTypes('terms', ['array']) 69 | ->setDefaults([ 70 | 'label' => 'setono_sylius_terms.form.terms_accept.label', 71 | 'mapped' => false, 72 | 'terms' => [], 73 | 'error_message' => 'setono_sylius_terms.terms_checkbox.required', 74 | ]) 75 | ; 76 | } 77 | 78 | public function getBlockPrefix(): string 79 | { 80 | return 'setono_sylius_terms_terms_checkbox_collection'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Form/Type/TermsTranslationType.php: -------------------------------------------------------------------------------- 1 | add('name', TextType::class, [ 18 | 'label' => 'setono_sylius_terms.form.terms.name', 19 | 'help' => 'setono_sylius_terms.form.terms.name_help', 20 | ]) 21 | ->add('slug', TextType::class, [ 22 | 'label' => 'setono_sylius_terms.form.terms.slug', 23 | ]) 24 | ->add('label', TextType::class, [ 25 | 'label' => 'setono_sylius_terms.form.terms.label', 26 | 'help' => 'setono_sylius_terms.form.terms.label_help', 27 | ]) 28 | ->add('content', TextareaType::class, [ 29 | 'required' => false, 30 | 'label' => 'setono_sylius_terms.form.terms.content', 31 | 'help' => 'setono_sylius_terms.form.terms.content_help', 32 | ]) 33 | ; 34 | } 35 | 36 | public function getBlockPrefix(): string 37 | { 38 | return 'setono_sylius_terms_terms_translation'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Form/Type/TermsType.php: -------------------------------------------------------------------------------- 1 | > $forms */ 19 | private array $forms = []; 20 | 21 | /** 22 | * @param array, array{label: string}> $forms 23 | * @param list $validationGroups 24 | */ 25 | public function __construct(string $dataClass, array $forms, array $validationGroups = []) 26 | { 27 | parent::__construct($dataClass, $validationGroups); 28 | 29 | foreach ($forms as $form => $config) { 30 | $this->forms[$config['label']] = $form; 31 | } 32 | } 33 | 34 | public function buildForm(FormBuilderInterface $builder, array $options): void 35 | { 36 | $builder 37 | ->add('enabled', CheckboxType::class, [ 38 | 'label' => 'sylius.ui.enabled', 39 | 'required' => false, 40 | ]) 41 | ->add('channels', ChannelChoiceType::class, [ 42 | 'multiple' => true, 43 | 'expanded' => true, 44 | 'label' => 'setono_sylius_terms.form.terms.channels', 45 | 'required' => false, 46 | ]) 47 | ->add('forms', ChoiceType::class, [ 48 | 'label' => 'setono_sylius_terms.form.terms.forms', 49 | 'choices' => $this->forms, 50 | 'required' => false, 51 | 'expanded' => true, 52 | 'multiple' => true, 53 | ]) 54 | ->add('translations', ResourceTranslationsType::class, [ 55 | 'entry_type' => TermsTranslationType::class, 56 | 'label' => 'setono_sylius_terms.form.terms.translations', 57 | ]) 58 | ->addEventSubscriber(new AddCodeFormSubscriber(null, [ 59 | 'label' => 'setono_sylius_terms.form.terms.code', 60 | ])) 61 | ; 62 | } 63 | 64 | public function getBlockPrefix(): string 65 | { 66 | return 'setono_sylius_terms_terms'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Model/Terms.php: -------------------------------------------------------------------------------- 1 | */ 28 | protected Collection $channels; 29 | 30 | /** @var list|null */ 31 | protected ?array $forms = []; 32 | 33 | public function __construct() 34 | { 35 | $this->channels = new ArrayCollection(); 36 | 37 | $this->initializeTranslationsCollection(); 38 | } 39 | 40 | public function getId(): ?int 41 | { 42 | return $this->id; 43 | } 44 | 45 | public function getCode(): ?string 46 | { 47 | return $this->code; 48 | } 49 | 50 | public function setCode(?string $code): void 51 | { 52 | $this->code = $code; 53 | } 54 | 55 | public function getName(): ?string 56 | { 57 | return $this->getTranslation()->getName(); 58 | } 59 | 60 | public function setName(?string $name): void 61 | { 62 | $this->getTranslation()->setName($name); 63 | } 64 | 65 | public function getSlug(): ?string 66 | { 67 | return $this->getTranslation()->getSlug(); 68 | } 69 | 70 | public function setSlug(?string $slug): void 71 | { 72 | $this->getTranslation()->setSlug($slug); 73 | } 74 | 75 | public function getLabel(): ?string 76 | { 77 | return $this->getTranslation()->getLabel(); 78 | } 79 | 80 | public function setLabel(?string $label): void 81 | { 82 | $this->getTranslation()->setLabel($label); 83 | } 84 | 85 | public function getContent(): ?string 86 | { 87 | return $this->getTranslation()->getContent(); 88 | } 89 | 90 | public function setContent(?string $content): void 91 | { 92 | $this->getTranslation()->setContent($content); 93 | } 94 | 95 | public function getChannels(): Collection 96 | { 97 | return $this->channels; 98 | } 99 | 100 | public function hasChannel(ChannelInterface $channel): bool 101 | { 102 | return $this->channels->contains($channel); 103 | } 104 | 105 | public function addChannel(ChannelInterface $channel): void 106 | { 107 | if (!$this->hasChannel($channel)) { 108 | $this->channels->add($channel); 109 | } 110 | } 111 | 112 | public function removeChannel(ChannelInterface $channel): void 113 | { 114 | if ($this->hasChannel($channel)) { 115 | $this->channels->removeElement($channel); 116 | } 117 | } 118 | 119 | public function getForms(): array 120 | { 121 | return $this->forms ?? []; 122 | } 123 | 124 | public function setForms(array $forms): void 125 | { 126 | $this->forms = null; 127 | 128 | foreach ($forms as $form) { 129 | $this->addForm($form); 130 | } 131 | } 132 | 133 | public function addForm(string $form): void 134 | { 135 | if (null === $this->forms) { 136 | $this->forms = []; 137 | } 138 | 139 | if (in_array($form, $this->forms, true)) { 140 | return; 141 | } 142 | 143 | $this->forms[] = $form; 144 | } 145 | 146 | public function removeForm(string $form): void 147 | { 148 | if (null === $this->forms) { 149 | return; 150 | } 151 | 152 | $forms = array_values(array_filter($this->forms, static fn (string $f) => $f !== $form)); 153 | 154 | $this->forms = [] === $forms ? null : $forms; 155 | } 156 | 157 | public function getTranslation(?string $locale = null): TermsTranslationInterface 158 | { 159 | /** @var TermsTranslationInterface $translation */ 160 | $translation = $this->doGetTranslation($locale); 161 | 162 | return $translation; 163 | } 164 | 165 | protected function createTranslation(): TermsTranslationInterface 166 | { 167 | return new TermsTranslation(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Model/TermsInterface.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | public function getForms(): array; 38 | 39 | /** 40 | * @param list $forms 41 | */ 42 | public function setForms(array $forms): void; 43 | 44 | /** 45 | * @param class-string $form 46 | */ 47 | public function addForm(string $form): void; 48 | 49 | public function removeForm(string $form): void; 50 | 51 | public function getTranslation(?string $locale = null): TermsTranslationInterface; 52 | } 53 | -------------------------------------------------------------------------------- /src/Model/TermsTranslation.php: -------------------------------------------------------------------------------- 1 | getName(); 25 | } 26 | 27 | public function getId(): ?int 28 | { 29 | return $this->id; 30 | } 31 | 32 | public function getName(): ?string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function setName(?string $name): void 38 | { 39 | $this->name = $name; 40 | } 41 | 42 | public function getSlug(): ?string 43 | { 44 | return $this->slug; 45 | } 46 | 47 | public function setSlug(?string $slug): void 48 | { 49 | $this->slug = $slug; 50 | } 51 | 52 | public function getLabel(): ?string 53 | { 54 | return $this->label; 55 | } 56 | 57 | public function setLabel(?string $label): void 58 | { 59 | $this->label = $label; 60 | } 61 | 62 | public function getContent(): ?string 63 | { 64 | return $this->content; 65 | } 66 | 67 | public function setContent(?string $content): void 68 | { 69 | $this->content = $content; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Model/TermsTranslationInterface.php: -------------------------------------------------------------------------------- 1 | termsRepository->findByFormAndChannelAndLocale( 23 | $form, 24 | $this->channelContext->getChannel(), 25 | $this->localeContext->getLocaleCode(), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Provider/TermsProviderInterface.php: -------------------------------------------------------------------------------- 1 | $form 17 | * 18 | * @return TermsInterface[] 19 | */ 20 | public function getTerms(string $form): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Renderer/LabelRenderer.php: -------------------------------------------------------------------------------- 1 | getLabel(); 20 | Assert::notNull($label); 21 | 22 | // if the terms has no content, we don't need to create a link because the terms will be empty 23 | if ($terms->getContent() === null || '' === $terms->getContent()) { 24 | return $label; 25 | } 26 | 27 | $slug = $terms->getSlug(); 28 | Assert::notNull($slug); 29 | 30 | $label = htmlspecialchars($label); 31 | 32 | $link = $this->urlGenerator->generate('setono_sylius_terms_shop_show_terms', ['slug' => $slug]); 33 | 34 | $replaced = (string) preg_replace_callback('/\[link:(.*?)\]/', static fn ($matches): string => sprintf( 35 | '%s', 36 | $link, 37 | $matches[1], 38 | ), $label); 39 | 40 | if ($replaced !== $label) { 41 | return $replaced; 42 | } 43 | 44 | return sprintf('%s', $link, $label); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Renderer/LabelRendererInterface.php: -------------------------------------------------------------------------------- 1 | ... tag 13 | */ 14 | public function render(TermsInterface $terms): string; 15 | } 16 | -------------------------------------------------------------------------------- /src/Repository/TermsRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('o') 22 | ->addSelect('translation') 23 | ->innerJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale') 24 | ->andWhere('o.enabled = true') 25 | ->andWhere('o.forms IS NOT NULL') 26 | ->andWhere('o.forms LIKE :form') 27 | ->andWhere(':channel MEMBER OF o.channels') 28 | ->setParameter('locale', $locale) 29 | ->setParameter('form', '%"' . $form . '"%') 30 | ->setParameter('channel', $channel) 31 | ->getQuery() 32 | ->getResult() 33 | ; 34 | 35 | Assert::isArray($objs); 36 | Assert::allIsInstanceOf($objs, TermsInterface::class); 37 | 38 | return $objs; 39 | } 40 | 41 | public function findOneByChannelAndLocaleAndSlug(ChannelInterface $channel, string $locale, string $slug): ?TermsInterface 42 | { 43 | $obj = $this->createQueryBuilder('o') 44 | ->addSelect('translation') 45 | ->innerJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale') 46 | ->andWhere('translation.slug = :slug') 47 | ->andWhere(':channel MEMBER OF o.channels') 48 | ->andWhere('o.enabled = true') 49 | ->setParameter('channel', $channel) 50 | ->setParameter('locale', $locale) 51 | ->setParameter('slug', $slug) 52 | ->getQuery() 53 | ->getOneOrNullResult() 54 | ; 55 | 56 | Assert::nullOrIsInstanceOf($obj, TermsInterface::class); 57 | 58 | return $obj; 59 | } 60 | 61 | public function findOneByChannelAndLocaleAndCode( 62 | ChannelInterface $channel, 63 | string $locale, 64 | string $code, 65 | ): ?TermsInterface { 66 | $obj = $this->createQueryBuilder('o') 67 | ->addSelect('translation') 68 | ->innerJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale') 69 | ->andWhere(':channel MEMBER OF o.channels') 70 | ->andWhere('o.code = :code') 71 | ->andWhere('o.enabled = true') 72 | ->setParameter('locale', $locale) 73 | ->setParameter('channel', $channel) 74 | ->setParameter('code', $code) 75 | ->getQuery() 76 | ->getOneOrNullResult() 77 | ; 78 | 79 | Assert::nullOrIsInstanceOf($obj, TermsInterface::class); 80 | 81 | return $obj; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Repository/TermsRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | $form 16 | * 17 | * @return array 18 | */ 19 | public function findByFormAndChannelAndLocale(string $form, ChannelInterface $channel, string $locale): array; 20 | 21 | public function findOneByChannelAndLocaleAndSlug(ChannelInterface $channel, string $locale, string $slug): ?TermsInterface; 22 | 23 | public function findOneByChannelAndLocaleAndCode(ChannelInterface $channel, string $locale, string $code): ?TermsInterface; 24 | } 25 | -------------------------------------------------------------------------------- /src/Resources/config/app/fixtures.yaml: -------------------------------------------------------------------------------- 1 | sylius_fixtures: 2 | suites: 3 | default: 4 | fixtures: 5 | 6 | # Adding some extra locales/currencies/channels 7 | # to show how terms working 8 | 9 | # Shipping/payment methods also required to be defined 10 | # for each additional channel 11 | 12 | locale: 13 | options: 14 | locales: 15 | # - en_US # already there 16 | - de_DE 17 | - fr_FR 18 | - uk_UA 19 | 20 | currency: 21 | options: 22 | currencies: 23 | # - USD # already there 24 | # - EUR # already there 25 | - UAH 26 | 27 | channel: 28 | options: 29 | custom: 30 | eu_web_store: 31 | name: "EU Web Store" 32 | code: EU_WEB 33 | locales: 34 | - en_US 35 | - de_DE 36 | - fr_FR 37 | currencies: 38 | - EUR 39 | enabled: true 40 | hostname: "localhost" 41 | ua_web_store: 42 | name: "UA Web Store" 43 | code: UA_WEB 44 | locales: 45 | - en_US 46 | - uk_UA 47 | base_currency: USD 48 | currencies: 49 | - USD 50 | - UAH 51 | enabled: true 52 | hostname: "localhost" 53 | 54 | payment_method: 55 | options: 56 | custom: 57 | eu_bank_transfer: 58 | code: eu_bank_transfer 59 | name: "EU Bank transfer" 60 | channels: 61 | - EU_WEB 62 | - UA_WEB 63 | enabled: true 64 | 65 | shipping_method: 66 | options: 67 | custom: 68 | eu_courier: 69 | code: eu_courier 70 | name: "EU courier" 71 | enabled: true 72 | channels: 73 | - EU_WEB 74 | - UA_WEB 75 | 76 | setono_terms: 77 | options: 78 | custom: 79 | terms_eula_us: 80 | code: eula_us 81 | name: EULA for United States 82 | slug: eula-us 83 | label: Accept US EULA terms 84 | channels: 85 | - FASHION_WEB 86 | terms_eula_eu: 87 | code: eula_eu 88 | name: EULA for EU 89 | slug: eula-eu 90 | label: Accept [link:EU EULA terms] 91 | translations: 92 | fr_FR: 93 | name: CLUF 94 | slug: cluf 95 | channels: 96 | - EU_WEB 97 | terms_terms_and_conditions: 98 | code: terms_and_conditions 99 | name: Terms and Conditions 100 | slug: terms-conditions 101 | label: Accept [link:Terms and conditions] 102 | translations: 103 | fr_FR: 104 | name: Termes et conditions 105 | slug: termes-conditions 106 | de_DE: 107 | name: Geschäftsbedingungen 108 | channels: 109 | - FASHION_WEB 110 | - EU_WEB 111 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine/model/Terms.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine/model/TermsTranslation.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/config/routes.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms_shop: 2 | resource: "@SetonoSyliusTermsPlugin/Resources/config/routes/shop.yaml" 3 | prefix: /{_locale} 4 | requirements: 5 | _locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$ 6 | 7 | setono_sylius_terms_admin: 8 | resource: "@SetonoSyliusTermsPlugin/Resources/config/routes/admin.yaml" 9 | prefix: /admin 10 | -------------------------------------------------------------------------------- /src/Resources/config/routes/admin.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms_admin_terms: 2 | resource: | 3 | alias: setono_sylius_terms.terms 4 | section: admin 5 | templates: "@SyliusAdmin\\Crud" 6 | redirect: update 7 | grid: setono_sylius_terms_terms 8 | vars: 9 | all: 10 | subheader: setono_sylius_terms.ui.manage_terms 11 | templates: 12 | form: "@SetonoSyliusTermsPlugin/admin/terms/_form.html.twig" 13 | index: 14 | icon: 'check circle outline' 15 | type: sylius.resource 16 | -------------------------------------------------------------------------------- /src/Resources/config/routes/shop.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms_shop_show_terms: 2 | path: '/%setono_sylius_terms.terms_path%/{slug}' 3 | methods: [GET] 4 | defaults: 5 | _controller: setono_sylius_terms.controller.action.show_terms 6 | -------------------------------------------------------------------------------- /src/Resources/config/routes_no_locale.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms_shop: 2 | resource: "@SetonoSyliusTermsPlugin/Resources/config/routes/shop.yaml" 3 | 4 | setono_sylius_terms_admin: 5 | resource: "@SetonoSyliusTermsPlugin/Resources/config/routes/admin.yaml" 6 | prefix: /admin 7 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Resources/config/services/controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Resources/config/services/event_subscriber.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Resources/config/services/fixture.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Resources/config/services/form.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | setono_sylius_terms 8 | 9 | 10 | setono_sylius_terms 11 | 12 | 13 | 14 | 15 | 17 | %setono_sylius_terms.model.terms.class% 18 | %setono_sylius_terms.forms% 19 | %setono_sylius_terms.form.type.terms.validation_groups% 20 | 21 | 22 | 23 | 24 | 26 | %setono_sylius_terms.model.terms_translation.class% 27 | %setono_sylius_terms.form.type.terms_translation.validation_groups% 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | %setono_sylius_terms.forms% 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Resources/config/services/provider.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Resources/config/services/renderer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Resources/config/services/twig.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Resources/config/validation/Terms.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Resources/config/validation/TermsTranslation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Resources/public/slugger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied from https://jsfiddle.net/ARTsinn/J4BRX/ 3 | */ 4 | (function () { 5 | 6 | // some defaults 7 | var options = { 8 | sensitive: false, 9 | replacement: "-", 10 | truncate: false 11 | }; 12 | 13 | // regex 14 | var nonWord = /\W/g, 15 | nonAlpha = /[^a-z0-9-]/g, 16 | whitespace = /\s+/g, 17 | trim = /^\s+|\s+$/g; 18 | 19 | // replacement charmap 20 | var map = { 21 | "À": "A", 22 | "Á": "A", 23 | "Â": "A", 24 | "Ã": "A", 25 | "Ä": "A", 26 | "Å": "A", 27 | "Æ": "AE", 28 | "Ç": "C", 29 | "È": "E", 30 | "É": "E", 31 | "Ê": "E", 32 | "Ë": "E", 33 | "Ì": "I", 34 | "Í": "I", 35 | "Î": "I", 36 | "Ï": "I", 37 | "Ð": "D", 38 | "Ñ": "N", 39 | "Ò": "O", 40 | "Ó": "O", 41 | "Ô": "O", 42 | "Õ": "O", 43 | "Ö": "O", 44 | "Ø": "O", 45 | "Ù": "U", 46 | "Ú": "U", 47 | "Û": "U", 48 | "Ü": "U", 49 | "Ý": "Y", 50 | "ß": "s", 51 | "à": "a", 52 | "á": "a", 53 | "â": "a", 54 | "ã": "a", 55 | "ä": "a", 56 | "å": "a", 57 | "æ": "ae", 58 | "ç": "c", 59 | "è": "e", 60 | "é": "e", 61 | "ê": "e", 62 | "ë": "e", 63 | "ì": "i", 64 | "í": "i", 65 | "î": "i", 66 | "ï": "i", 67 | "ñ": "n", 68 | "ò": "o", 69 | "ó": "o", 70 | "ô": "o", 71 | "õ": "o", 72 | "ö": "o", 73 | "ø": "o", 74 | "ù": "u", 75 | "ú": "u", 76 | "û": "u", 77 | "ü": "u", 78 | "ý": "y", 79 | "ÿ": "y", 80 | "Ā": "A", 81 | "ā": "a", 82 | "Ă": "A", 83 | "ă": "a", 84 | "Ą": "A", 85 | "ą": "a", 86 | "Ć": "C", 87 | "ć": "c", 88 | "Ĉ": "C", 89 | "ĉ": "c", 90 | "Ċ": "C", 91 | "ċ": "c", 92 | "Č": "C", 93 | "č": "c", 94 | "Ď": "D", 95 | "ď": "d", 96 | "Đ": "D", 97 | "đ": "d", 98 | "Ē": "E", 99 | "ē": "e", 100 | "Ĕ": "E", 101 | "ĕ": "e", 102 | "Ė": "E", 103 | "ė": "e", 104 | "Ę": "E", 105 | "ę": "e", 106 | "Ě": "E", 107 | "ě": "e", 108 | "Ĝ": "G", 109 | "ĝ": "g", 110 | "Ğ": "G", 111 | "ğ": "g", 112 | "Ġ": "G", 113 | "ġ": "g", 114 | "Ģ": "G", 115 | "ģ": "g", 116 | "Ĥ": "H", 117 | "ĥ": "h", 118 | "Ħ": "H", 119 | "ħ": "h", 120 | "Ĩ": "I", 121 | "ĩ": "i", 122 | "Ī": "I", 123 | "ī": "i", 124 | "Ĭ": "I", 125 | "ĭ": "i", 126 | "Į": "I", 127 | "į": "i", 128 | "İ": "I", 129 | "ı": "i", 130 | "IJ": "IJ", 131 | "ij": "ij", 132 | "Ĵ": "J", 133 | "ĵ": "j", 134 | "Ķ": "K", 135 | "ķ": "k", 136 | "Ĺ": "L", 137 | "ĺ": "l", 138 | "Ļ": "L", 139 | "ļ": "l", 140 | "Ľ": "L", 141 | "ľ": "l", 142 | "Ŀ": "L", 143 | "ŀ": "l", 144 | "Ł": "l", 145 | "ł": "l", 146 | "Ń": "N", 147 | "ń": "n", 148 | "Ņ": "N", 149 | "ņ": "n", 150 | "Ň": "N", 151 | "ň": "n", 152 | "ʼn": "n", 153 | "Ō": "O", 154 | "ō": "o", 155 | "Ŏ": "O", 156 | "ŏ": "o", 157 | "Ő": "O", 158 | "ő": "o", 159 | "Œ": "OE", 160 | "œ": "oe", 161 | "Ŕ": "R", 162 | "ŕ": "r", 163 | "Ŗ": "R", 164 | "ŗ": "r", 165 | "Ř": "R", 166 | "ř": "r", 167 | "Ś": "S", 168 | "ś": "s", 169 | "Ŝ": "S", 170 | "ŝ": "s", 171 | "Ş": "S", 172 | "ş": "s", 173 | "Š": "S", 174 | "š": "s", 175 | "Ţ": "T", 176 | "ţ": "t", 177 | "Ť": "T", 178 | "ť": "t", 179 | "Ŧ": "T", 180 | "ŧ": "t", 181 | "Ũ": "U", 182 | "ũ": "u", 183 | "Ū": "U", 184 | "ū": "u", 185 | "Ŭ": "U", 186 | "ŭ": "u", 187 | "Ů": "U", 188 | "ů": "u", 189 | "Ű": "U", 190 | "ű": "u", 191 | "Ų": "U", 192 | "ų": "u", 193 | "Ŵ": "W", 194 | "ŵ": "w", 195 | "Ŷ": "Y", 196 | "ŷ": "y", 197 | "Ÿ": "Y", 198 | "Ź": "Z", 199 | "ź": "z", 200 | "Ż": "Z", 201 | "ż": "z", 202 | "Ž": "Z", 203 | "ž": "z", 204 | "ſ": "s", 205 | "ƒ": "f", 206 | "Ơ": "O", 207 | "ơ": "o", 208 | "Ư": "U", 209 | "ư": "u", 210 | "Ǎ": "A", 211 | "ǎ": "a", 212 | "Ǐ": "I", 213 | "ǐ": "i", 214 | "Ǒ": "O", 215 | "ǒ": "o", 216 | "Ǔ": "U", 217 | "ǔ": "u", 218 | "Ǖ": "U", 219 | "ǖ": "u", 220 | "Ǘ": "U", 221 | "ǘ": "u", 222 | "Ǚ": "U", 223 | "ǚ": "u", 224 | "Ǜ": "U", 225 | "ǜ": "u", 226 | "Ǻ": "A", 227 | "ǻ": "a", 228 | "Ǽ": "AE", 229 | "ǽ": "ae", 230 | "Ǿ": "O", 231 | "ǿ": "o" 232 | }; 233 | 234 | // replacer 235 | var mapping = function (c) { 236 | return map[c] || c; 237 | }; 238 | 239 | 240 | /** 241 | * Normalise a string replacing foreign characters 242 | * whitespace and all other illegals 243 | * @param {String} str 244 | * @param {Object} opts 245 | */ 246 | 247 | this.slugger = function (str, opts) { 248 | if (!str || typeof str !== "string") return; 249 | 250 | // populate some defaults 251 | if (opts) for (var key in opts) options[key] = opts[key]; 252 | 253 | // case-sensitive or not 254 | if (!options.sensitive) str = str.toLowerCase(); 255 | 256 | str = str 257 | 258 | // trim whitespace 259 | .replace(trim, "") 260 | 261 | // swap foreign characters 262 | .replace(nonWord, mapping) 263 | 264 | // replace whitespace 265 | .replace(whitespace, options.replacement) 266 | 267 | // remove everything but alphanumeric characters and dashes 268 | .replace(nonAlpha, "") 269 | 270 | // replace multiple instances of the replacement character with a single instance 271 | .replace(new RegExp(`[${options.replacement}]+`, 'g'), options.replacement) 272 | ; 273 | 274 | // smart truncate 275 | if (options.truncate && str.length > options.truncate) { 276 | var cut = str.indexOf("-", options.truncate); 277 | str = cut === -1 ? str : str.slice(0, cut); 278 | } 279 | 280 | return str; 281 | }; 282 | }()); 283 | -------------------------------------------------------------------------------- /src/Resources/public/slugify-terms-name.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function (e) { 2 | document.querySelectorAll('input[name*="setono_sylius_terms_terms[translations]"][name*="[name]"]').forEach(function (input) { 3 | input.addEventListener('input', function (event) { 4 | const element = event.currentTarget; 5 | element.closest('.content').querySelector('[name*="[slug]"]').value = slugger(element.value); 6 | }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.da.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | ui: 3 | terms: Betingelser 4 | manage_terms: Administrer betingelser 5 | new_terms: Opret betingelser 6 | edit_terms: Rediger betingelser 7 | 8 | code: Kode 9 | name: Navn 10 | channels: Kanaler 11 | 12 | menu: 13 | admin: 14 | main: 15 | configuration: 16 | terms: Betingelser 17 | 18 | form: 19 | terms_accept: 20 | label: Accepter alle betingelser nedenfor for at gennemføre ordren 21 | terms_should_be_accepted: "{{ name }} skal accepteres for at gennemføre ordren" 22 | terms: 23 | translations: Oversættelser 24 | name: Navn 25 | name_help: Navnet bruges både som reference og som linktekst, når betingelseslinket vises 26 | code: Kode 27 | slug: Slug 28 | label: Tekst til afkrydsningsfelt 29 | label_help: Brug [link:Linktekst] til at fortælle hvor linket skal være i teksten 30 | content: Indhold 31 | content_help: Hvis indholdet er tomt vil teksten ved siden af afkrydsningsfeltet ikke have et link 32 | channels: Kanaler 33 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.de.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | ui: 3 | terms: Konditionen 4 | manage_terms: Verwalte Konditionen 5 | new_terms: Neue Konditionen 6 | edit_terms: Bearbeite Konditionen 7 | 8 | code: Code 9 | name: Name 10 | channels: Kanäle 11 | 12 | menu: 13 | admin: 14 | main: 15 | configuration: 16 | terms: Konditionen 17 | 18 | form: 19 | terms_accept: 20 | label: Akzeptieren Sie alle Konditionen um die Bestellung abzuschließen 21 | terms_should_be_accepted: "{{ name }} muss akzeptiert werden" 22 | terms: 23 | translations: Übersetzungen 24 | name: Name 25 | name_help: Der Name wird sowohl als Referenz, als auch als Linktext verwendet, wenn der Konditionen-Link angezeigt wird 26 | code: Code 27 | slug: Slug 28 | label: Etikett 29 | label_help: Verwenden Sie [link:Link text], um anzugeben wo im Text der Link eingefügt werden muss 30 | content: Inhalt 31 | content_help: Wenn der Inhalt leer ist, wird das Etikett neben dem Kontrollkästchen keinen Link enthalten 32 | channels: Kanäle 33 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.en.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | ui: 3 | terms: Terms 4 | manage_terms: Manage terms 5 | new_terms: New terms 6 | edit_terms: Edit terms 7 | 8 | code: Code 9 | name: Name 10 | channels: Channels 11 | 12 | menu: 13 | admin: 14 | main: 15 | configuration: 16 | terms: Terms 17 | 18 | form: 19 | terms_accept: 20 | label: Accept all terms below to complete an order 21 | terms_should_be_accepted: "{{ name }} should be accepted" 22 | terms: 23 | translations: Translations 24 | name: Name 25 | name_help: The name is used both as a reference, but also as the link text when showing the terms link 26 | code: Code 27 | slug: Slug 28 | label: Label 29 | label_help: Use [link:Link text] to tell where the link should be placed in the text 30 | content: Content 31 | content_help: If the content is empty the label next to the checkbox will not have a link 32 | channels: Channels 33 | forms: Forms 34 | term_form: 35 | complete: Checkout > Complete order 36 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.fr.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | ui: 3 | terms: Conditions 4 | manage_terms: Gérer les conditions 5 | new_terms: Nouvelles conditions 6 | edit_terms: Modifier les conditions 7 | 8 | code: Code 9 | name: Nom 10 | channels: Canaux 11 | 12 | menu: 13 | admin: 14 | main: 15 | configuration: 16 | terms: Conditions 17 | 18 | form: 19 | terms_accept: 20 | label: Veuillez accepter les conditions pour finaliser la commande 21 | terms_should_be_accepted: "{{ name }} doit être accepté pour finaliser la commande" 22 | terms: 23 | translations: Traductions 24 | name: Nom 25 | name_help: Le nom est utilisé à la fois comme référence, mais aussi comme texte de lien lors de l'affichage du lien des conditions 26 | code: Code 27 | slug: Slug 28 | label: Étiquette 29 | label_help: "Utiliser [link:Link text] pour indiquer l'endroit où le lien doit être placé dans le texte." 30 | content: Contenu 31 | content_help: Si le contenu est vide, l'étiquette à côté de la case à cocher n'aura pas de lien 32 | channels: Canaux 33 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.nl.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | ui: 3 | terms: Voorwaarden 4 | manage_terms: Beheer voorwaarden 5 | new_terms: Nieuwe voorwaarden 6 | edit_terms: Bewerk voorwaarden 7 | 8 | code: Code 9 | name: Naam 10 | channels: Kanalen 11 | 12 | menu: 13 | admin: 14 | main: 15 | configuration: 16 | terms: Voorwaarden 17 | 18 | form: 19 | terms_accept: 20 | label: Accepteer alle voorwaarden om de bestelling af te ronden 21 | terms_should_be_accepted: "{{ name }} dient te worden geaccepteerd" 22 | terms: 23 | translations: Vertalingen 24 | name: Naam 25 | name_help: De naam wordt zowel als referentie, maar ook als linktekst gebruikt bij het tonen van de voorwaarden link 26 | code: Code 27 | slug: Slug 28 | label: Etiket 29 | label_help: Gebruik [link:Link text] om aan te geven waar in de tekst de link geplaatst dient te worden 30 | content: Content 31 | content_help: Als de content leeg is, zal het etiket naast het selectievakje geen link bevatten 32 | channels: Kanalen 33 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.da.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | terms: 3 | code: 4 | regex: Koden kan kun bestå af bogstaver, tal, bindestreger og underscores. 5 | unique: Denne kode er allerede i brug. 6 | slug: 7 | unique: Sluggen skal være unik. 8 | terms_checkbox: 9 | required: Du skal acceptere betingelserne for at fortsætte. 10 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.de.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | terms: 3 | code: 4 | regex: Der Code für die Konditionen darf lediglich aus Buchstaben, Zahlen, Bindestrichen und Unterstrichen bestehen. 5 | unique: Der Code wird bereits verwendet. 6 | slug: 7 | unique: Der Slug wird bereits verwendet. 8 | terms_checkbox: 9 | required: Sie müssen die Konditionen akzeptieren, um fortzufahren. 10 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.en.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | terms: 3 | code: 4 | regex: Term code can only be comprised of letters, numbers, dashes and underscores. 5 | unique: This code is already in use. 6 | slug: 7 | unique: Terms slug must be unique. 8 | terms_checkbox: 9 | required: You must accept the terms to continue. 10 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.fr.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | terms: 3 | code: 4 | regex: Le code pour les conditions ne peut comporter que des lettres, des chiffres, - et _. 5 | unique: Le code est déjà utilisé. 6 | slug: 7 | unique: "L'identifiant est déjà utilisé." 8 | terms_checkbox: 9 | required: Vous devez accepter les conditions pour continuer. 10 | -------------------------------------------------------------------------------- /src/Resources/translations/validators.nl.yaml: -------------------------------------------------------------------------------- 1 | setono_sylius_terms: 2 | terms: 3 | code: 4 | regex: De code voor de voorwaarden mag uitsluitend bestaan uit letters, cijfers, - en _. 5 | unique: De code is al in gebruik. 6 | slug: 7 | unique: De slug is al in gebruik. 8 | terms_checkbox: 9 | required: U moet de voorwaarden accepteren om door te gaan. 10 | -------------------------------------------------------------------------------- /src/Resources/views/admin/grid/field/channels.html.twig: -------------------------------------------------------------------------------- 1 |
    2 | {% for channel in data %} 3 |
  • 4 | {{ channel.name }} 5 |
  • 6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /src/Resources/views/admin/terms/_form.html.twig: -------------------------------------------------------------------------------- 1 | {% from '@SyliusAdmin/Macro/translationForm.html.twig' import translationFormWithSlug %} 2 | 3 |
4 |
5 | {{ form_errors(form) }} 6 | 7 |
8 | {{ form_row(form.code) }} 9 | {{ form_row(form.enabled) }} 10 |
11 | {{ form_row(form.forms) }} 12 | {{ form_row(form.channels) }} 13 |
14 | 15 |
16 |
17 |
18 | {{ translationFormWithSlug(form.translations, '@SyliusAdmin/Product/_slugField.html.twig', terms) }} 19 |
20 |
21 | -------------------------------------------------------------------------------- /src/Resources/views/admin/terms/_javascripts.html.twig: -------------------------------------------------------------------------------- 1 | {% include '@SyliusUi/_javascripts.html.twig' with {'path': '/bundles/setonosyliustermsplugin/slugger.js'} %} 2 | {% include '@SyliusUi/_javascripts.html.twig' with {'path': '/bundles/setonosyliustermsplugin/slugify-terms-name.js'} %} 3 | -------------------------------------------------------------------------------- /src/Resources/views/shop/terms/link.html.twig: -------------------------------------------------------------------------------- 1 | {# @var terms \Setono\SyliusTermsPlugin\Model\TermsInterface #} 2 | {{ terms.name }} 3 | -------------------------------------------------------------------------------- /src/Resources/views/shop/terms/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@SyliusShop/layout.html.twig' %} 2 | 3 | {%- block title -%} 4 | {{ terms.name }} 5 | {%- endblock -%} 6 | 7 | {% block content %} 8 | {% include '@SetonoSyliusTermsPlugin/shop/terms/show/_breadcrumb.html.twig' %} 9 | 10 | 11 | 12 |
13 |
14 |

{{ terms.name }}

15 | 16 | {{ terms.content|raw }} 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/Resources/views/shop/terms/show/_breadcrumb.html.twig: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/SetonoSyliusTermsPlugin.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getFunctions(): array 16 | { 17 | return [ 18 | new TwigFunction('terms_link', [TermsRuntime::class, 'link'], ['needs_environment' => true, 'is_safe' => ['html']]), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Twig/TermsRuntime.php: -------------------------------------------------------------------------------- 1 | ... link to the terms if the given terms code exists 24 | * else it returns an empty string 25 | */ 26 | public function link(Environment $env, string $code, string $template = null): string 27 | { 28 | $terms = $this->termsRepository->findOneByChannelAndLocaleAndCode( 29 | $this->channelContext->getChannel(), 30 | $this->localeContext->getLocaleCode(), 31 | $code, 32 | ); 33 | 34 | $template ??= '@SetonoSyliusTermsPlugin/shop/terms/link.html.twig'; 35 | 36 | return $env->render($template, [ 37 | 'terms' => $terms, 38 | ]); 39 | } 40 | } 41 | --------------------------------------------------------------------------------