From 1228e994cb29ff52749db70d803be42edaefecae Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Fri, 6 May 2022 14:52:43 -0400 Subject: [PATCH] [Autocompleter] New Ajax-powered, autocomplete component --- package.json | 5 +- rollup.config.js | 6 + src/Autocomplete/.gitattributes | 1 + src/Autocomplete/.gitignore | 5 + src/Autocomplete/.symfony.bundle.yaml | 3 + src/Autocomplete/LICENSE | 21 + src/Autocomplete/README.md | 16 + src/Autocomplete/assets/.gitignore | 1 + src/Autocomplete/assets/dist/controller.js | 165 ++++++ src/Autocomplete/assets/jest.config.js | 5 + src/Autocomplete/assets/package.json | 30 ++ src/Autocomplete/assets/src/controller.ts | 197 +++++++ .../assets/test/controller.test.ts | 140 +++++ src/Autocomplete/assets/test/setup.js | 12 + src/Autocomplete/composer.json | 58 ++ src/Autocomplete/phpunit.xml.dist | 34 ++ src/Autocomplete/src/AutocompleteBundle.php | 29 + .../src/AutocompleteResultsExecutor.php | 60 +++ .../src/AutocompleterRegistry.php | 37 ++ .../EntityAutocompleteController.php | 47 ++ .../AutocompleteExtension.php | 148 ++++++ .../AutocompleteFormTypePass.php | 86 +++ .../src/Doctrine/DoctrineRegistryWrapper.php | 50 ++ .../src/Doctrine/EntityMetadata.php | 68 +++ .../src/Doctrine/EntityMetadataFactory.php | 56 ++ .../src/Doctrine/EntitySearchUtil.php | 127 +++++ .../src/Doctrine/SearchEscaper.php | 59 +++ .../src/EntityAutocompleterInterface.php | 56 ++ .../src/Form/AsEntityAutocompleteField.php | 55 ++ .../Form/AutocompleteChoiceTypeExtension.php | 102 ++++ .../Form/AutocompleteEntityTypeSubscriber.php | 74 +++ .../src/Form/ParentEntityAutocompleteType.php | 89 ++++ .../Form/WrappedEntityTypeAutocompleter.php | 118 +++++ .../src/Maker/MakeAutocompleteField.php | 155 ++++++ .../src/Maker/MakerAutocompleteVariables.php | 28 + .../src/Resources/doc/food-non-ajax.png | Bin 0 -> 50176 bytes src/Autocomplete/src/Resources/doc/index.rst | 494 ++++++++++++++++++ .../doc/ux-autocomplete-animation.gif | Bin 0 -> 117888 bytes src/Autocomplete/src/Resources/routes.php | 9 + .../skeletons/AutocompleteField.tpl.php | 39 ++ .../translations/AutocompleteBundle.ar.php | 6 + .../translations/AutocompleteBundle.bg.php | 6 + .../translations/AutocompleteBundle.ca.php | 6 + .../translations/AutocompleteBundle.cs.php | 6 + .../translations/AutocompleteBundle.da.php | 6 + .../translations/AutocompleteBundle.de.php | 6 + .../translations/AutocompleteBundle.el.php | 6 + .../translations/AutocompleteBundle.en.php | 6 + .../translations/AutocompleteBundle.es.php | 6 + .../translations/AutocompleteBundle.eu.php | 6 + .../translations/AutocompleteBundle.fa.php | 6 + .../translations/AutocompleteBundle.fi.php | 6 + .../translations/AutocompleteBundle.fr.php | 6 + .../translations/AutocompleteBundle.gl.php | 6 + .../translations/AutocompleteBundle.hr.php | 6 + .../translations/AutocompleteBundle.hu.php | 6 + .../translations/AutocompleteBundle.id.php | 6 + .../translations/AutocompleteBundle.it.php | 6 + .../translations/AutocompleteBundle.lb.php | 6 + .../translations/AutocompleteBundle.lt.php | 6 + .../translations/AutocompleteBundle.nl.php | 6 + .../translations/AutocompleteBundle.pl.php | 6 + .../translations/AutocompleteBundle.pt.php | 6 + .../translations/AutocompleteBundle.pt_BR.php | 6 + .../translations/AutocompleteBundle.ro.php | 6 + .../translations/AutocompleteBundle.ru.php | 6 + .../translations/AutocompleteBundle.sl.php | 6 + .../translations/AutocompleteBundle.sr_RS.php | 6 + .../translations/AutocompleteBundle.sv.php | 6 + .../translations/AutocompleteBundle.tr.php | 6 + .../translations/AutocompleteBundle.uk.php | 6 + .../translations/AutocompleteBundle.zh_CN.php | 6 + .../views/autocomplete_form_theme.html.twig | 9 + .../CustomProductAutocompleter.php | 55 ++ .../tests/Fixtures/Entity/Category.php | 75 +++ .../tests/Fixtures/Entity/Product.php | 97 ++++ .../Fixtures/Factory/CategoryFactory.php | 47 ++ .../tests/Fixtures/Factory/ProductFactory.php | 55 ++ .../Form/CategoryAutocompleteType.php | 48 ++ .../tests/Fixtures/Form/ProductType.php | 49 ++ src/Autocomplete/tests/Fixtures/Kernel.php | 148 ++++++ .../tests/Fixtures/templates/form.html.twig | 5 + .../AutocompleteFormRenderingTest.php | 63 +++ .../Functional/CustomAutocompleterTest.php | 107 ++++ .../Functional/FieldAutocompleterTest.php | 81 +++ .../Doctrine/EntityMetadataFactoryTest.php | 28 + .../Doctrine/EntityMetadataTest.php | 85 +++ .../Doctrine/EntitySearchUtilTest.php | 83 +++ .../tests/Integration/WiringTest.php | 49 ++ .../Unit/AutocompleteResultsExecutorTest.php | 95 ++++ src/Autocomplete/tests/bootstrap.php | 16 + .../assets/dist/register_controller.js | 2 +- .../assets/dist/render_controller.js | 36 +- 93 files changed, 4093 insertions(+), 17 deletions(-) create mode 100644 src/Autocomplete/.gitattributes create mode 100644 src/Autocomplete/.gitignore create mode 100644 src/Autocomplete/.symfony.bundle.yaml create mode 100644 src/Autocomplete/LICENSE create mode 100644 src/Autocomplete/README.md create mode 100644 src/Autocomplete/assets/.gitignore create mode 100644 src/Autocomplete/assets/dist/controller.js create mode 100644 src/Autocomplete/assets/jest.config.js create mode 100644 src/Autocomplete/assets/package.json create mode 100644 src/Autocomplete/assets/src/controller.ts create mode 100644 src/Autocomplete/assets/test/controller.test.ts create mode 100644 src/Autocomplete/assets/test/setup.js create mode 100644 src/Autocomplete/composer.json create mode 100644 src/Autocomplete/phpunit.xml.dist create mode 100644 src/Autocomplete/src/AutocompleteBundle.php create mode 100644 src/Autocomplete/src/AutocompleteResultsExecutor.php create mode 100644 src/Autocomplete/src/AutocompleterRegistry.php create mode 100644 src/Autocomplete/src/Controller/EntityAutocompleteController.php create mode 100644 src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php create mode 100644 src/Autocomplete/src/DependencyInjection/AutocompleteFormTypePass.php create mode 100644 src/Autocomplete/src/Doctrine/DoctrineRegistryWrapper.php create mode 100644 src/Autocomplete/src/Doctrine/EntityMetadata.php create mode 100644 src/Autocomplete/src/Doctrine/EntityMetadataFactory.php create mode 100644 src/Autocomplete/src/Doctrine/EntitySearchUtil.php create mode 100644 src/Autocomplete/src/Doctrine/SearchEscaper.php create mode 100644 src/Autocomplete/src/EntityAutocompleterInterface.php create mode 100644 src/Autocomplete/src/Form/AsEntityAutocompleteField.php create mode 100644 src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php create mode 100644 src/Autocomplete/src/Form/AutocompleteEntityTypeSubscriber.php create mode 100644 src/Autocomplete/src/Form/ParentEntityAutocompleteType.php create mode 100644 src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php create mode 100644 src/Autocomplete/src/Maker/MakeAutocompleteField.php create mode 100644 src/Autocomplete/src/Maker/MakerAutocompleteVariables.php create mode 100644 src/Autocomplete/src/Resources/doc/food-non-ajax.png create mode 100644 src/Autocomplete/src/Resources/doc/index.rst create mode 100644 src/Autocomplete/src/Resources/doc/ux-autocomplete-animation.gif create mode 100644 src/Autocomplete/src/Resources/routes.php create mode 100644 src/Autocomplete/src/Resources/skeletons/AutocompleteField.tpl.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.ar.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.bg.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.ca.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.cs.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.da.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.de.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.el.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.en.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.es.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.eu.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.fa.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.fi.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.fr.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.gl.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.hr.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.hu.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.id.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.it.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.lb.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.lt.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.nl.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.pl.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt_BR.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.ro.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.ru.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.sl.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.sr_RS.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.sv.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.tr.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.uk.php create mode 100644 src/Autocomplete/src/Resources/translations/AutocompleteBundle.zh_CN.php create mode 100644 src/Autocomplete/src/Resources/views/autocomplete_form_theme.html.twig create mode 100644 src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php create mode 100644 src/Autocomplete/tests/Fixtures/Entity/Category.php create mode 100644 src/Autocomplete/tests/Fixtures/Entity/Product.php create mode 100644 src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php create mode 100644 src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php create mode 100644 src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php create mode 100644 src/Autocomplete/tests/Fixtures/Form/ProductType.php create mode 100644 src/Autocomplete/tests/Fixtures/Kernel.php create mode 100644 src/Autocomplete/tests/Fixtures/templates/form.html.twig create mode 100644 src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php create mode 100644 src/Autocomplete/tests/Functional/CustomAutocompleterTest.php create mode 100644 src/Autocomplete/tests/Functional/FieldAutocompleterTest.php create mode 100644 src/Autocomplete/tests/Integration/Doctrine/EntityMetadataFactoryTest.php create mode 100644 src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php create mode 100644 src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php create mode 100644 src/Autocomplete/tests/Integration/WiringTest.php create mode 100644 src/Autocomplete/tests/Unit/AutocompleteResultsExecutorTest.php create mode 100644 src/Autocomplete/tests/bootstrap.php diff --git a/package.json b/package.json index ed9d85209a9..817fbe80478 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "@babel/preset-env": "^7.15.8", "@babel/preset-react": "^7.15.8", "@babel/preset-typescript": "^7.15.8", - "@rollup/plugin-node-resolve": "^13.0.0", + "@rollup/plugin-commonjs": "^22.0.0", + "@rollup/plugin-node-resolve": "^13.0.6", "@rollup/plugin-typescript": "^8.3.0", "@symfony/stimulus-testing": "^2.0.1", "@typescript-eslint/eslint-plugin": "^5.2.0", @@ -29,7 +30,7 @@ "jest": "^27.3.1", "pkg-up": "^3.1.0", "prettier": "^2.2.1", - "rollup": "^2.52.2", + "rollup": "^2.68.0", "tslib": "^2.3.1", "typescript": "^4.4.4" }, diff --git a/rollup.config.js b/rollup.config.js index 9b814f97d61..c3b1f1d102c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,5 @@ import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import glob from 'glob'; import path from 'path'; @@ -56,6 +57,11 @@ const packages = files.map((file) => { plugins: [ resolve(), typescript(), + commonjs({ + namedExports: { + 'react-dom/client': ['createRoot'], + }, + }), wildcardExternalsPlugin(peerDependencies) ], }; diff --git a/src/Autocomplete/.gitattributes b/src/Autocomplete/.gitattributes new file mode 100644 index 00000000000..18e14aad7f0 --- /dev/null +++ b/src/Autocomplete/.gitattributes @@ -0,0 +1 @@ +/tests export-ignore diff --git a/src/Autocomplete/.gitignore b/src/Autocomplete/.gitignore new file mode 100644 index 00000000000..854217846fe --- /dev/null +++ b/src/Autocomplete/.gitignore @@ -0,0 +1,5 @@ +/composer.lock +/phpunit.xml +/vendor/ +/var/ +/.phpunit.result.cache diff --git a/src/Autocomplete/.symfony.bundle.yaml b/src/Autocomplete/.symfony.bundle.yaml new file mode 100644 index 00000000000..17001f559e7 --- /dev/null +++ b/src/Autocomplete/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "src/Resources/doc" diff --git a/src/Autocomplete/LICENSE b/src/Autocomplete/LICENSE new file mode 100644 index 00000000000..45c069b323b --- /dev/null +++ b/src/Autocomplete/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Autocomplete/README.md b/src/Autocomplete/README.md new file mode 100644 index 00000000000..d0374b9dd43 --- /dev/null +++ b/src/Autocomplete/README.md @@ -0,0 +1,16 @@ +# UX Autocomplete + +Javascript-powered auto-completion functionality for your Symfony forms! + +**EXPERIMENTAL** This component is currently experimental and is +likely to change, or even change drastically. + +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. + +## Resources + +- [Documentation](https://symfony.com/bundles/ux-autocomplete/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Autocomplete/assets/.gitignore b/src/Autocomplete/assets/.gitignore new file mode 100644 index 00000000000..2ccbe4656c6 --- /dev/null +++ b/src/Autocomplete/assets/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js new file mode 100644 index 00000000000..a25187c2374 --- /dev/null +++ b/src/Autocomplete/assets/dist/controller.js @@ -0,0 +1,165 @@ +import { Controller } from '@hotwired/stimulus'; +import TomSelect from 'tom-select'; + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __classPrivateFieldGet(receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +} + +var _instances, _getCommonConfig, _createAutocomplete, _createAutocompleteWithHtmlContents, _createAutocompleteWithRemoteData, _stripTags, _mergeObjects, _createTomSelect, _dispatchEvent; +class default_1 extends Controller { + constructor() { + super(...arguments); + _instances.add(this); + } + connect() { + if (this.tomSelect) { + return; + } + if (this.urlValue) { + this.tomSelect = __classPrivateFieldGet(this, _instances, "m", _createAutocompleteWithRemoteData).call(this, this.urlValue); + return; + } + if (this.optionsAsHtmlValue) { + this.tomSelect = __classPrivateFieldGet(this, _instances, "m", _createAutocompleteWithHtmlContents).call(this); + return; + } + this.tomSelect = __classPrivateFieldGet(this, _instances, "m", _createAutocomplete).call(this); + } + get selectElement() { + if (!(this.element instanceof HTMLSelectElement)) { + return null; + } + return this.element; + } + get formElement() { + if (!(this.element instanceof HTMLInputElement) && !(this.element instanceof HTMLSelectElement)) { + throw new Error('Autocomplete Stimulus controller can only be used no an or or + `); + + expect(getByTestId(container, 'main-element')).not.toHaveClass('pre-connected'); + expect(getByTestId(container, 'main-element')).not.toHaveClass('connected'); + + application = startStimulus(); + + await waitFor(() => { + expect(getByTestId(container, 'main-element')).toHaveClass('pre-connected'); + expect(getByTestId(container, 'main-element')).toHaveClass('connected'); + }); + + const tomSelect = getByTestId(container, 'main-element').tomSelect; + expect(tomSelect.input).toBe(getByTestId(container, 'main-element')); + }); + + it('connect with ajax URL', async () => { + const container = mountDOM(` + + + `); + + application = startStimulus(); + + await waitFor(() => { + expect(getByTestId(container, 'main-element')).toHaveClass('connected'); + }); + + // initial Ajax request on focus + fetchMock.mock( + '/path/to/autocomplete?query=', + JSON.stringify({ + results: [ + { + value: 3, + text: 'salad' + }, + ], + }), + ); + + fetchMock.mock( + '/path/to/autocomplete?query=foo', + JSON.stringify({ + results: [ + { + value: 1, + text: 'pizza' + }, + { + value: 2, + text: 'popcorn' + }, + ], + }), + ); + + const tomSelect = getByTestId(container, 'main-element').tomSelect; + const controlInput = tomSelect.control_input; + + // wait for the initial Ajax request to finish + userEvent.click(controlInput); + await waitFor(() => { + expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(1); + }); + + // typing was not properly triggering, for some reason + //userEvent.type(controlInput, 'foo'); + controlInput.value = 'foo'; + controlInput.dispatchEvent(new Event('input')); + + await waitFor(() => { + expect(container.querySelectorAll('.option[data-selectable]')).toHaveLength(2); + }); + }); +}); diff --git a/src/Autocomplete/assets/test/setup.js b/src/Autocomplete/assets/test/setup.js new file mode 100644 index 00000000000..af928c33cf4 --- /dev/null +++ b/src/Autocomplete/assets/test/setup.js @@ -0,0 +1,12 @@ +/* + * This file is part of the Symfony Live Component package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// adds the missing "fetch" function - fetch-mock-jest will replace this +// eslint-disable-next-line +global.fetch = require('node-fetch'); diff --git a/src/Autocomplete/composer.json b/src/Autocomplete/composer.json new file mode 100644 index 00000000000..df4032610c8 --- /dev/null +++ b/src/Autocomplete/composer.json @@ -0,0 +1,58 @@ +{ + "name": "symfony/ux-autocomplete", + "type": "symfony-bundle", + "description": "JavaScript Autocomplete functionality for Symfony", + "keywords": [ + "symfony-ux" + ], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "autoload": { + "psr-4": { + "Symfony\\UX\\Autocomplete\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Symfony\\UX\\Autocomplete\\Tests\\": "tests/" + } + }, + "require": { + "php": ">=8.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^2.0", + "doctrine/orm": "^2.7", + "mtdowling/jmespath.php": "2.6.x-dev", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/maker-bundle": "^1.40", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "zenstruck/browser": "^1.1", + "zenstruck/foundry": "^1.19" + }, + "config": { + "sort-packages": true + }, + "extra": { + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "minimum-stability": "dev" +} diff --git a/src/Autocomplete/phpunit.xml.dist b/src/Autocomplete/phpunit.xml.dist new file mode 100644 index 00000000000..cf4b7344ac7 --- /dev/null +++ b/src/Autocomplete/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + ./tests/ + + + + + + ./src + + + + + + + diff --git a/src/Autocomplete/src/AutocompleteBundle.php b/src/Autocomplete/src/AutocompleteBundle.php new file mode 100644 index 00000000000..e278ab90970 --- /dev/null +++ b/src/Autocomplete/src/AutocompleteBundle.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\UX\Autocomplete\DependencyInjection\AutocompleteFormTypePass; + +/** + * @author Ryan Weaver + * + * @experimental + */ +final class AutocompleteBundle extends Bundle +{ + public function build(ContainerBuilder $container) + { + $container->addCompilerPass(new AutocompleteFormTypePass()); + } +} diff --git a/src/Autocomplete/src/AutocompleteResultsExecutor.php b/src/Autocomplete/src/AutocompleteResultsExecutor.php new file mode 100644 index 00000000000..264b79df409 --- /dev/null +++ b/src/Autocomplete/src/AutocompleteResultsExecutor.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete; + +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Security; +use Symfony\UX\Autocomplete\Doctrine\DoctrineRegistryWrapper; +use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil; + +/** + * @author Ryan Weaver + * + * @experimental + */ +final class AutocompleteResultsExecutor +{ + public function __construct( + private EntitySearchUtil $entitySearchUtil, + private DoctrineRegistryWrapper $managerRegistry, + private ?Security $security = null + ) { + } + + public function fetchResults(EntityAutocompleterInterface $autocompleter, string $query): array + { + if ($this->security && !$autocompleter->isGranted($this->security)) { + throw new AccessDeniedException('Access denied from autocompleter class.'); + } + + $queryBuilder = $autocompleter->getQueryBuilder($this->managerRegistry->getRepository($autocompleter->getEntityClass())); + $searchableProperties = $autocompleter->getSearchableFields(); + $this->entitySearchUtil->addSearchClause($queryBuilder, $query, $autocompleter->getEntityClass(), $searchableProperties); + + // if no max is set, set one + if (!$queryBuilder->getMaxResults()) { + $queryBuilder->setMaxResults(10); + } + + $entities = $queryBuilder->getQuery()->execute(); + + $results = []; + foreach ($entities as $entity) { + $results[] = [ + 'value' => $autocompleter->getValue($entity), + 'text' => $autocompleter->getLabel($entity), + ]; + } + + return $results; + } +} diff --git a/src/Autocomplete/src/AutocompleterRegistry.php b/src/Autocomplete/src/AutocompleterRegistry.php new file mode 100644 index 00000000000..732ec30edcf --- /dev/null +++ b/src/Autocomplete/src/AutocompleterRegistry.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete; + +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @author Ryan Weaver + * + * @experimental + */ +final class AutocompleterRegistry +{ + public function __construct( + private ServiceLocator $autocompletersLocator + ) { + } + + public function getAutocompleter(string $alias): ?EntityAutocompleterInterface + { + return $this->autocompletersLocator->has($alias) ? $this->autocompletersLocator->get($alias) : null; + } + + public function getAutocompleterNames(): array + { + return array_keys($this->autocompletersLocator->getProvidedServices()); + } +} diff --git a/src/Autocomplete/src/Controller/EntityAutocompleteController.php b/src/Autocomplete/src/Controller/EntityAutocompleteController.php new file mode 100644 index 00000000000..91e6e014553 --- /dev/null +++ b/src/Autocomplete/src/Controller/EntityAutocompleteController.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Controller; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\UX\Autocomplete\AutocompleteResultsExecutor; +use Symfony\UX\Autocomplete\AutocompleterRegistry; + +/** + * @author Ryan Weaver + * + * @experimental + */ +final class EntityAutocompleteController +{ + public function __construct( + private AutocompleterRegistry $autocompleteFieldRegistry, + private AutocompleteResultsExecutor $autocompleteResultsExecutor + ) { + } + + public function __invoke(string $alias, Request $request): Response + { + $autocompleter = $this->autocompleteFieldRegistry->getAutocompleter($alias); + if (!$autocompleter) { + throw new NotFoundHttpException(sprintf('No autocompleter found for "%s". Available autocompleters are: (%s)', $alias, implode(', ', $this->autocompleteFieldRegistry->getAutocompleterNames()))); + } + + $results = $this->autocompleteResultsExecutor->fetchResults($autocompleter, $request->query->get('query', '')); + + return new JsonResponse([ + 'results' => $results, + ]); + } +} diff --git a/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php b/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php new file mode 100644 index 00000000000..f8e0d82ee9e --- /dev/null +++ b/src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Form\Form; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\UX\Autocomplete\AutocompleteResultsExecutor; +use Symfony\UX\Autocomplete\AutocompleterRegistry; +use Symfony\UX\Autocomplete\Controller\EntityAutocompleteController; +use Symfony\UX\Autocomplete\Doctrine\DoctrineRegistryWrapper; +use Symfony\UX\Autocomplete\Doctrine\EntityMetadataFactory; +use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil; +use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField; +use Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension; +use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType; +use Symfony\UX\Autocomplete\Form\WrappedEntityTypeAutocompleter; +use Symfony\UX\Autocomplete\Maker\MakeAutocompleteField; +use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg; + +/** + * @author Ryan Weaver + * + * @experimental + */ +final class AutocompleteExtension extends Extension implements PrependExtensionInterface +{ + public function prepend(ContainerBuilder $container) + { + $bundles = $container->getParameter('kernel.bundles'); + + if (!isset($bundles['TwigBundle'])) { + return; + } + + $container->prependExtensionConfig('twig', [ + 'form_themes' => ['@Autocomplete/autocomplete_form_theme.html.twig'], + ]); + } + + public function load(array $configs, ContainerBuilder $container) + { + $this->registerBasicServices($container); + if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle'])) { + $this->registerFormServices($container); + } + } + + private function registerBasicServices(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration(AsEntityAutocompleteField::class, function (Definition $definition) { + $definition->addTag(AutocompleteFormTypePass::ENTITY_AUTOCOMPLETE_FIELD_TAG); + }); + + $container + ->register('ux.autocomplete.autocompleter_registry', AutocompleterRegistry::class) + ->setArguments([ + abstract_arg('autocompleter service locator'), + ]); + + $container + ->register('ux.autocomplete.doctrine_registry_wrapper', DoctrineRegistryWrapper::class) + ->setArguments([ + new Reference('doctrine', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + ]) + ; + + $container + ->register('ux.autocomplete.results_executor', AutocompleteResultsExecutor::class) + ->setArguments([ + new Reference('ux.autocomplete.entity_search_util'), + new Reference('ux.autocomplete.doctrine_registry_wrapper'), + new Reference('security.helper', ContainerInterface::NULL_ON_INVALID_REFERENCE), + ]) + ; + + $container + ->register('ux.autocomplete.entity_search_util', EntitySearchUtil::class) + ->setArguments([ + new Reference('ux.autocomplete.entity_metadata_factory'), + ]) + ; + + $container + ->register('ux.autocomplete.entity_metadata_factory', EntityMetadataFactory::class) + ->setArguments([ + new Reference('ux.autocomplete.doctrine_registry_wrapper'), + ]) + ; + + $container + ->register('ux.autocomplete.entity_autocomplete_controller', EntityAutocompleteController::class) + ->setArguments([ + new Reference('ux.autocomplete.autocompleter_registry'), + new Reference('ux.autocomplete.results_executor'), + ]) + ->addTag('controller.service_arguments') + ; + + $container + ->register('ux.autocomplete.make_autocomplete_field', MakeAutocompleteField::class) + ->setArguments([ + new Reference('maker.doctrine_helper', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + ]) + ->addTag('maker.command') + ; + } + + private function registerFormServices(ContainerBuilder $container): void + { + $container + ->register('ux.autocomplete.entity_type', ParentEntityAutocompleteType::class) + ->setArguments([ + new Reference('router'), + ]) + ->addTag('form.type'); + + $container + ->register('ux.autocomplete.choice_type_extension', AutocompleteChoiceTypeExtension::class) + ->setArguments([ + new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + ]) + ->addTag('form.type_extension'); + + $container + ->register('ux.autocomplete.wrapped_entity_type_autocompleter', WrappedEntityTypeAutocompleter::class) + ->setAbstract(true) + ->setArguments([ + abstract_arg('form type string'), + new Reference('form.factory'), + new Reference('ux.autocomplete.entity_metadata_factory'), + new Reference('property_accessor'), + ]); + } +} diff --git a/src/Autocomplete/src/DependencyInjection/AutocompleteFormTypePass.php b/src/Autocomplete/src/DependencyInjection/AutocompleteFormTypePass.php new file mode 100644 index 00000000000..e5cf657113d --- /dev/null +++ b/src/Autocomplete/src/DependencyInjection/AutocompleteFormTypePass.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\DependencyInjection; + +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField; + +/** + * @author Ryan Weaver + * + * @experimental + */ +class AutocompleteFormTypePass implements CompilerPassInterface +{ + /** @var string Tag applied to form types that will be used for autocompletion */ + public const ENTITY_AUTOCOMPLETE_FIELD_TAG = 'ux.entity_autocomplete_field'; + /** @var string Tag applied to EntityAutocompleterInterface classes */ + public const ENTITY_AUTOCOMPLETER_TAG = 'ux.entity_autocompleter'; + + public function process(ContainerBuilder $container) + { + $this->processEntityAutocompleteFieldTag($container); + $this->processEntityAutocompleterTag($container); + } + + private function processEntityAutocompleteFieldTag(ContainerBuilder $container) + { + foreach ($container->findTaggedServiceIds(self::ENTITY_AUTOCOMPLETE_FIELD_TAG, true) as $serviceId => $tag) { + $serviceDefinition = $container->getDefinition($serviceId); + if (!$serviceDefinition->hasTag('form.type')) { + throw new \LogicException(sprintf('Service "%s" has the "%s" tag, but is not tagged with "form.type". Did you add the "%s" attribute to a class that is not a form type?', $serviceId, self::ENTITY_AUTOCOMPLETE_FIELD_TAG, AsEntityAutocompleteField::class)); + } + $alias = $this->getAlias($serviceId, $serviceDefinition, $tag); + + $wrappedDefinition = (new ChildDefinition('ux.autocomplete.wrapped_entity_type_autocompleter')) + // the "formType" string + ->replaceArgument(0, $serviceDefinition->getClass()) + ->addTag(self::ENTITY_AUTOCOMPLETER_TAG, ['alias' => $alias]); + $container->setDefinition('ux.autocomplete.wrapped_entity_type_autocompleter.'.$alias, $wrappedDefinition); + } + } + + private function getAlias(string $serviceId, Definition $serviceDefinition, array $tag): string + { + if ($tag[0]['alias'] ?? null) { + return $tag[0]['alias']; + } + + $class = $serviceDefinition->getClass(); + $attribute = AsEntityAutocompleteField::getInstance($class); + if (null === $attribute) { + throw new \LogicException(sprintf('The service "%s" either needs to have the #[%s] attribute above its class or its "%s" tag needs an "alias" key.', $serviceId, self::ENTITY_AUTOCOMPLETE_FIELD_TAG, AsEntityAutocompleteField::class)); + } + + return $attribute->getAlias() ?: AsEntityAutocompleteField::shortName($class); + } + + private function processEntityAutocompleterTag(ContainerBuilder $container) + { + $servicesMap = []; + foreach ($container->findTaggedServiceIds(self::ENTITY_AUTOCOMPLETER_TAG, true) as $serviceId => $tag) { + if (!isset($tag[0]['alias'])) { + throw new \LogicException(sprintf('The "%s" tag of the "%s" service needs "alias" key.', self::ENTITY_AUTOCOMPLETER_TAG, $serviceId)); + } + + $servicesMap[$tag[0]['alias']] = new Reference($serviceId); + } + + $definition = $container->findDefinition('ux.autocomplete.autocompleter_registry'); + $definition->setArgument(0, ServiceLocatorTagPass::register($container, $servicesMap)); + } +} diff --git a/src/Autocomplete/src/Doctrine/DoctrineRegistryWrapper.php b/src/Autocomplete/src/Doctrine/DoctrineRegistryWrapper.php new file mode 100644 index 00000000000..2beeb54db6b --- /dev/null +++ b/src/Autocomplete/src/Doctrine/DoctrineRegistryWrapper.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Doctrine; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Symfony\Bridge\Doctrine\ManagerRegistry; + +/** + * Small wrapper around ManagerRegistry to help if Doctrine is missing. + * + * @author Ryan Weaver + * + * @experimental + */ +class DoctrineRegistryWrapper +{ + public function __construct( + private ?ManagerRegistry $registry = null + ) { + } + + public function getRepository(string $class): EntityRepository + { + return $this->getRegistry()->getRepository($class); + } + + public function getManagerForClass(string $class): EntityManagerInterface + { + return $this->getRegistry()->getManagerForClass($class); + } + + private function getRegistry(): ManagerRegistry + { + if (null === $this->registry) { + throw new \LogicException('Doctrine must be installed to use the entity features of AutocompleteBundle.'); + } + + return $this->registry; + } +} diff --git a/src/Autocomplete/src/Doctrine/EntityMetadata.php b/src/Autocomplete/src/Doctrine/EntityMetadata.php new file mode 100644 index 00000000000..2cd3594fb1f --- /dev/null +++ b/src/Autocomplete/src/Doctrine/EntityMetadata.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Doctrine; + +use Doctrine\Persistence\Mapping\ClassMetadata; + +/** + * @author Ryan Weaver + * + * @experimental + */ +class EntityMetadata +{ + public function __construct( + private ClassMetadata $metadata + ) { + } + + public function getAllPropertyNames(): array + { + return $this->metadata->getFieldNames(); + } + + public function isAssociation(string $propertyName): bool + { + return \array_key_exists($propertyName, $this->metadata->associationMappings) + || (str_contains($propertyName, '.') && !$this->isEmbeddedClassProperty($propertyName)); + } + + public function isEmbeddedClassProperty(string $propertyName): bool + { + $propertyNameParts = explode('.', $propertyName, 2); + + return \array_key_exists($propertyNameParts[0], $this->metadata->embeddedClasses); + } + + public function getPropertyMetadata(string $propertyName): array + { + if (\array_key_exists($propertyName, $this->metadata->fieldMappings)) { + return $this->metadata->fieldMappings[$propertyName]; + } + + if (\array_key_exists($propertyName, $this->metadata->associationMappings)) { + return $this->metadata->associationMappings[$propertyName]; + } + + throw new \InvalidArgumentException(sprintf('The "%s" field does not exist in the "%s" entity.', $propertyName, $this->getFqcn())); + } + + public function getPropertyDataType(string $propertyName): string + { + return $this->getPropertyMetadata($propertyName)['type']; + } + + public function getIdValue(object $entity): string + { + return current($this->metadata->getIdentifierValues($entity)); + } +} diff --git a/src/Autocomplete/src/Doctrine/EntityMetadataFactory.php b/src/Autocomplete/src/Doctrine/EntityMetadataFactory.php new file mode 100644 index 00000000000..b2838280f4a --- /dev/null +++ b/src/Autocomplete/src/Doctrine/EntityMetadataFactory.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Doctrine; + +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; + +/** + * Adapted from EasyCorp/EasyAdminBundle EntityFactory. + * + * @experimental + */ +class EntityMetadataFactory +{ + public function __construct( + private DoctrineRegistryWrapper $doctrine + ) { + } + + public function create(?string $entityFqcn): EntityMetadata + { + $entityMetadata = $this->getEntityMetadata($entityFqcn); + + return new EntityMetadata($entityMetadata); + } + + private function getEntityMetadata(string $entityFqcn): ClassMetadata + { + $entityManager = $this->getEntityManager($entityFqcn); + $entityMetadata = $entityManager->getClassMetadata($entityFqcn); + + if (1 !== \count($entityMetadata->getIdentifierFieldNames())) { + throw new \RuntimeException(sprintf('Autocomplete does not support Doctrine entities with composite primary keys (such as the ones used in the "%s" entity).', $entityFqcn)); + } + + return $entityMetadata; + } + + private function getEntityManager(string $entityFqcn): ObjectManager + { + if (null === $entityManager = $this->doctrine->getManagerForClass($entityFqcn)) { + throw new \RuntimeException(sprintf('There is no Doctrine Entity Manager defined for the "%s" class', $entityFqcn)); + } + + return $entityManager; + } +} diff --git a/src/Autocomplete/src/Doctrine/EntitySearchUtil.php b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php new file mode 100644 index 00000000000..da0029f0fad --- /dev/null +++ b/src/Autocomplete/src/Doctrine/EntitySearchUtil.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Doctrine; + +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Uid\Ulid; +use Symfony\Component\Uid\Uuid; + +/** + * Adapted from EasyCorp/EasyAdminBundle. + * + * @experimental + */ +class EntitySearchUtil +{ + public function __construct(private EntityMetadataFactory $metadataFactory) + { + } + + /** + * Adapted from easycorp/easyadmin EntityRepository. + */ + public function addSearchClause(QueryBuilder $queryBuilder, string $query, string $entityClass, array $searchableProperties = null): void + { + $entityMetadata = $this->metadataFactory->create($entityClass); + + $lowercaseQuery = mb_strtolower($query); + $isNumericQuery = is_numeric($query); + $isSmallIntegerQuery = ctype_digit($query) && $query >= -32768 && $query <= 32767; + $isIntegerQuery = ctype_digit($query) && $query >= -2147483648 && $query <= 2147483647; + $isUuidQuery = class_exists(Uuid::class) && Uuid::isValid($query); + $isUlidQuery = class_exists(Ulid::class) && Ulid::isValid($query); + + $dqlParameters = [ + // adding '0' turns the string into a numeric value + 'numeric_query' => is_numeric($query) ? 0 + $query : $query, + 'uuid_query' => $query, + 'text_query' => '%'.$lowercaseQuery.'%', + 'words_query' => explode(' ', $lowercaseQuery), + ]; + + $entitiesAlreadyJoined = []; + $searchableProperties = empty($searchableProperties) ? $entityMetadata->getAllPropertyNames() : $searchableProperties; + $expressions = []; + foreach ($searchableProperties as $propertyName) { + if ($entityMetadata->isAssociation($propertyName)) { + // support arbitrarily nested associations (e.g. foo.bar.baz.qux) + $associatedProperties = explode('.', $propertyName); + $numAssociatedProperties = \count($associatedProperties); + + if (1 === $numAssociatedProperties) { + throw new \InvalidArgumentException(sprintf('The "%s" property included in the setSearchFields() method is not a valid search field. When using associated properties in search, you must also define the exact field used in the search (e.g. \'%s.id\', \'%s.name\', etc.)', $propertyName, $propertyName, $propertyName)); + } + + $originalPropertyName = $associatedProperties[0]; + $originalPropertyMetadata = $entityMetadata->getPropertyMetadata($originalPropertyName); + $associatedEntityDto = $this->metadataFactory->create($originalPropertyMetadata['targetEntity']); + + for ($i = 0; $i < $numAssociatedProperties - 1; ++$i) { + $associatedEntityName = $associatedProperties[$i]; + $associatedEntityAlias = SearchEscaper::escapeDqlAlias($associatedEntityName); + $associatedPropertyName = $associatedProperties[$i + 1]; + + if (!\in_array($associatedEntityName, $entitiesAlreadyJoined, true)) { + $parentEntityName = 0 === $i ? $queryBuilder->getRootAliases()[0] : $associatedProperties[$i - 1]; + $queryBuilder->leftJoin($parentEntityName.'.'.$associatedEntityName, $associatedEntityAlias); + $entitiesAlreadyJoined[] = $associatedEntityName; + } + + if ($i < $numAssociatedProperties - 2) { + $propertyMetadata = $associatedEntityDto->getPropertyMetadata($associatedPropertyName); + $targetEntity = $propertyMetadata['targetEntity']; + $associatedEntityDto = $this->metadataFactory->create($targetEntity); + } + } + + $entityName = $associatedEntityAlias; + $propertyName = $associatedPropertyName; + $propertyDataType = $associatedEntityDto->getPropertyDataType($propertyName); + } else { + $entityName = $queryBuilder->getRootAliases()[0]; + $propertyDataType = $entityMetadata->getPropertyDataType($propertyName); + } + + $isSmallIntegerProperty = 'smallint' === $propertyDataType; + $isIntegerProperty = 'integer' === $propertyDataType; + $isNumericProperty = \in_array($propertyDataType, ['number', 'bigint', 'decimal', 'float']); + // 'citext' is a PostgreSQL extension (https://github.com/EasyCorp/EasyAdminBundle/issues/2556) + $isTextProperty = \in_array($propertyDataType, ['string', 'text', 'citext', 'array', 'simple_array']); + $isGuidProperty = \in_array($propertyDataType, ['guid', 'uuid']); + $isUlidProperty = 'ulid' === $propertyDataType; + + // this complex condition is needed to avoid issues on PostgreSQL databases + if ( + ($isSmallIntegerProperty && $isSmallIntegerQuery) || + ($isIntegerProperty && $isIntegerQuery) || + ($isNumericProperty && $isNumericQuery) + ) { + $expressions[] = $queryBuilder->expr()->eq(sprintf('%s.%s', $entityName, $propertyName), ':query_for_numbers'); + $queryBuilder->setParameter('query_for_numbers', $dqlParameters['numeric_query']); + } elseif ($isGuidProperty && $isUuidQuery) { + $expressions[] = $queryBuilder->expr()->eq(sprintf('%s.%s', $entityName, $propertyName), ':query_for_uuids'); + $queryBuilder->setParameter('query_for_uuids', $dqlParameters['uuid_query'], 'uuid' === $propertyDataType ? 'uuid' : null); + } elseif ($isUlidProperty && $isUlidQuery) { + $expressions[] = $queryBuilder->expr()->eq(sprintf('%s.%s', $entityName, $propertyName), ':query_for_uuids'); + $queryBuilder->setParameter('query_for_uuids', $dqlParameters['uuid_query'], 'ulid'); + } elseif ($isTextProperty) { + $expressions[] = $queryBuilder->expr()->like(sprintf('LOWER(%s.%s)', $entityName, $propertyName), ':query_for_text'); + $queryBuilder->setParameter('query_for_text', $dqlParameters['text_query']); + + $expressions[] = $queryBuilder->expr()->in(sprintf('LOWER(%s.%s)', $entityName, $propertyName), ':query_as_words'); + $queryBuilder->setParameter('query_as_words', $dqlParameters['words_query']); + } + } + + $queryBuilder->andWhere($queryBuilder->expr()->orX(...$expressions)); + } +} diff --git a/src/Autocomplete/src/Doctrine/SearchEscaper.php b/src/Autocomplete/src/Doctrine/SearchEscaper.php new file mode 100644 index 00000000000..5452c7c276d --- /dev/null +++ b/src/Autocomplete/src/Doctrine/SearchEscaper.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Doctrine; + +use Doctrine\ORM\Query\Lexer; + +/** + * Adapted from EasyCorp/EasyAdminBundle Escaper. + * + * @experimental + */ +class SearchEscaper +{ + public const DQL_ALIAS_PREFIX = 'autocomplete_'; + + /** + * Some words (e.g. "order") are reserved keywords in the DQL (Doctrine Query Language). + * That's why when using entity names as DQL aliases, we need to escape + * those reserved keywords. + * + * This method ensures that the given entity name can be used as a DQL alias. + * Most of them are left unchanged (e.g. "category" or "invoice") but others + * will include a prefix to escape them (e.g. "order" becomes "autocomplete_order"). + */ + public static function escapeDqlAlias(string $entityName): string + { + if (self::isDqlReservedKeyword($entityName)) { + return self::DQL_ALIAS_PREFIX.$entityName; + } + + return $entityName; + } + + /** + * Determines if a string is a reserved keyword in DQL (Doctrine Query Language). + */ + private static function isDqlReservedKeyword(string $string): bool + { + $lexer = new Lexer($string); + + $lexer->moveNext(); + $token = $lexer->lookahead; + + if (200 <= $token['type']) { + return true; + } + + return false; + } +} diff --git a/src/Autocomplete/src/EntityAutocompleterInterface.php b/src/Autocomplete/src/EntityAutocompleterInterface.php new file mode 100644 index 00000000000..50eebfb27a2 --- /dev/null +++ b/src/Autocomplete/src/EntityAutocompleterInterface.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Security\Core\Security; + +/** + * Interface for classes that will have an "autocomplete" endpoint exposed. + */ +interface EntityAutocompleterInterface +{ + /** + * The fully-qualified entity class this will be autocompleting. + */ + public function getEntityClass(): string; + + /** + * A query builder that would return all potential results. + */ + public function getQueryBuilder(EntityRepository $repository): QueryBuilder; + + /** + * Returns the "choice_label" used to display this entity. + */ + public function getLabel(object $entity): string; + + /** + * Returns the "value" attribute for this entity, usually the id. + */ + public function getValue(object $entity): mixed; + + /** + * Return an array of the fields to search. + * + * If null is returned, all fields are searched. + */ + public function getSearchableFields(): ?array; + + /** + * Return true if access should be granted to the autocomplete results for the current user. + * + * Note: if SecurityBundle is not installed, this will not be called. + */ + public function isGranted(Security $security): bool; +} diff --git a/src/Autocomplete/src/Form/AsEntityAutocompleteField.php b/src/Autocomplete/src/Form/AsEntityAutocompleteField.php new file mode 100644 index 00000000000..d1d1c13839a --- /dev/null +++ b/src/Autocomplete/src/Form/AsEntityAutocompleteField.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Form; + +use Symfony\Component\String\UnicodeString; + +/** + * All form types that want to expose autocomplete functionality should have this. + * + * @author Ryan Weaver + * + * @experimental + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsEntityAutocompleteField +{ + public function __construct( + private ?string $alias = null, + ) { + } + + public function getAlias(): ?string + { + return $this->alias; + } + + public static function shortName(string $class): string + { + $string = new UnicodeString($class); + + return $string->afterLast('\\')->snake()->toString(); + } + + public static function getInstance(string $class): ?self + { + $reflectionClass = new \ReflectionClass($class); + + $attributes = $reflectionClass->getAttributes(self::class); + + if (0 === \count($attributes)) { + return null; + } + + return $attributes[0]->newInstance(); + } +} diff --git a/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php new file mode 100644 index 00000000000..cb0b98d4538 --- /dev/null +++ b/src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Form; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Initializes the autocomplete Stimulus controller for any fields with the "autocomplete" option. + * + * @internal + */ +final class AutocompleteChoiceTypeExtension extends AbstractTypeExtension +{ + public function __construct(private ?TranslatorInterface $translator = null) + { + } + + public static function getExtendedTypes(): iterable + { + return [ + ChoiceType::class, + TextType::class, + ]; + } + + public function finishView(FormView $view, FormInterface $form, array $options) + { + if (!$options['autocomplete']) { + return; + } + + $attr = $view->vars['attr'] ?? []; + + $controllerName = 'symfony--ux-autocomplete--autocomplete'; + $attr['data-controller'] = trim(($attr['data-controller'] ?? '').' '.$controllerName); + + $values = []; + if ($options['autocomplete_url']) { + $values['url'] = $options['autocomplete_url']; + } + + if ($options['options_as_html']) { + $values['options-as-html'] = ''; + } + + if ($options['allow_options_create']) { + $values['allow-options-create'] = ''; + } + + if ($options['tom_select_options']) { + $values['tom-select-options'] = json_encode($options['tom_select_options']); + } + + $values['no-results-found-text'] = $this->trans($options['no_results_found_text']); + $values['no-more-results-text'] = $this->trans($options['no_more_results_text']); + + foreach ($values as $name => $value) { + $attr['data-'.$controllerName.'-'.$name.'-value'] = $value; + } + + $view->vars['attr'] = $attr; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'autocomplete' => false, + 'autocomplete_url' => null, + 'tom_select_options' => [], + 'options_as_html' => false, + 'allow_options_create' => false, + 'no_results_found_text' => 'No results found', + 'no_more_results_text' => 'No more results', + ]); + + // if autocomplete_url is passed, then HTML options are already supported + $resolver->setNormalizer('options_as_html', function (Options $options, $value) { + return null === $options['autocomplete_url'] ? $value : false; + }); + } + + private function trans(string $message): string + { + return $this->translator ? $this->translator->trans($message, [], 'AutocompleteBundle') : $message; + } +} diff --git a/src/Autocomplete/src/Form/AutocompleteEntityTypeSubscriber.php b/src/Autocomplete/src/Form/AutocompleteEntityTypeSubscriber.php new file mode 100644 index 00000000000..a2c77600208 --- /dev/null +++ b/src/Autocomplete/src/Form/AutocompleteEntityTypeSubscriber.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Form; + +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Helps transform ParentEntityAutocompleteType into a EntityType that will not load all options. + * + * @internal + */ +final class AutocompleteEntityTypeSubscriber implements EventSubscriberInterface +{ + public function __construct( + private ?string $autocompleteUrl = null + ) { + } + + public function preSetData(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData() ?: []; + + $options = $form->getConfig()->getOptions(); + $options['compound'] = false; + $options['choices'] = is_iterable($data) ? $data : [$data]; + // pass to AutocompleteChoiceTypeExtension + $options['autocomplete'] = true; + $options['autocomplete_url'] = $this->autocompleteUrl; + unset($options['searchable_fields'], $options['security']); + + $form->add('autocomplete', EntityType::class, $options); + } + + public function preSubmit(FormEvent $event) + { + $data = $event->getData(); + $form = $event->getForm(); + $options = $form->get('autocomplete')->getConfig()->getOptions(); + + if (!isset($data['autocomplete']) || '' === $data['autocomplete']) { + $options['choices'] = []; + } else { + $options['choices'] = $options['em']->getRepository($options['class'])->findBy([ + $options['id_reader']->getIdField() => $data['autocomplete'], + ]); + } + + // reset some critical lazy options + unset($options['em'], $options['loader'], $options['empty_data'], $options['choice_list'], $options['choices_as_values']); + + $form->add('autocomplete', EntityType::class, $options); + } + + public static function getSubscribedEvents(): array + { + return [ + FormEvents::PRE_SET_DATA => 'preSetData', + FormEvents::PRE_SUBMIT => 'preSubmit', + ]; + } +} diff --git a/src/Autocomplete/src/Form/ParentEntityAutocompleteType.php b/src/Autocomplete/src/Form/ParentEntityAutocompleteType.php new file mode 100644 index 00000000000..69652a6dd9b --- /dev/null +++ b/src/Autocomplete/src/Form/ParentEntityAutocompleteType.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * All form types that want to expose autocomplete functionality should use this for its getParent(). + */ +final class ParentEntityAutocompleteType extends AbstractType implements DataMapperInterface +{ + public function __construct( + private UrlGeneratorInterface $urlGenerator + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $formType = $builder->getType()->getInnerType(); + $attribute = AsEntityAutocompleteField::getInstance(\get_class($formType)); + if (!$attribute) { + throw new \LogicException(sprintf('The %s class must have a #[AsEntityAutocompleteField] attribute above its class.', \get_class($formType))); + } + + $autocompleteUrl = $this->urlGenerator->generate('ux_entity_autocomplete', [ + 'alias' => $attribute->getAlias() ?: AsEntityAutocompleteField::shortName(\get_class($formType)), + ]); + $builder + ->addEventSubscriber(new AutocompleteEntityTypeSubscriber($autocompleteUrl)) + ->setDataMapper($this); + } + + public function finishView(FormView $view, FormInterface $form, array $options) + { + // Add a custom block prefix to inner field to ease theming: + array_splice($view['autocomplete']->vars['block_prefixes'], -1, 0, 'ux_entity_autocomplete_inner'); + // this IS A compound (i.e. has children) field + // however, we only render the child "autocomplete" field. So for rendering, fake NOT compound + $view->vars['compound'] = false; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'multiple' => false, + // force display errors on this form field + 'error_bubbling' => false, + 'searchable_fields' => null, + // set to the string role that's required to view the autocomplete results + // or a callable: function(Symfony\Component\Security\Core\Security $security): bool + 'security' => false, + ]); + + $resolver->setRequired(['class']); + $resolver->setAllowedTypes('security', ['boolean', 'string', 'callable']); + } + + public function getBlockPrefix(): string + { + return 'ux_entity_autocomplete'; + } + + public function mapDataToForms($data, $forms) + { + $form = current(iterator_to_array($forms, false)); + $form->setData($data); + } + + public function mapFormsToData($forms, &$data) + { + $form = current(iterator_to_array($forms, false)); + $data = $form->getData(); + } +} diff --git a/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php b/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php new file mode 100644 index 00000000000..aa6fdeb0ddd --- /dev/null +++ b/src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php @@ -0,0 +1,118 @@ +getFormOption('class'); + } + + public function getQueryBuilder(EntityRepository $repository): QueryBuilder + { + if ($queryBuilder = $this->getFormOption('query_builder')) { + return $queryBuilder; + } + + return $repository->createQueryBuilder('e'); + } + + public function getLabel(object $entity): string + { + $choiceLabel = $this->getFormOption('choice_label'); + + if (null === $choiceLabel) { + return (string) $entity; + } + + if (\is_string($choiceLabel) || $choiceLabel instanceof PropertyPathInterface) { + return $this->propertyAccessor->getValue($entity, $choiceLabel); + } + + // 0 hardcoded as the "index", should not be relevant + return $choiceLabel($entity, 0, $this->getValue($entity)); + } + + public function getValue(object $entity): string + { + return $this->getEntityMetadata()->getIdValue($entity); + } + + public function getSearchableFields(): ?array + { + return $this->getForm()->getConfig()->getOption('searchable_fields'); + } + + public function isGranted(Security $security): bool + { + $securityOption = $this->getForm()->getConfig()->getOption('security'); + + if (false === $securityOption) { + return true; + } + + if (\is_string($securityOption)) { + return $security->isGranted($securityOption, $this); + } + + if (\is_callable($securityOption)) { + return $securityOption($security); + } + + throw new \InvalidArgumentException('Invalid passed to the "security" option: it must be the boolean true, a string role or a callable.'); + } + + private function getFormOption(string $name): mixed + { + $form = $this->getForm(); + $formOptions = $form['autocomplete']->getConfig()->getOptions(); + + return $formOptions[$name] ?? null; + } + + private function getForm(): FormInterface + { + if (null === $this->form) { + $this->form = $this->formFactory->create($this->formType); + } + + return $this->form; + } + + private function getEntityMetadata(): EntityMetadata + { + if (null === $this->entityMetadata) { + $this->entityMetadata = $this->metadataFactory->create($this->getEntityClass()); + } + + return $this->entityMetadata; + } +} diff --git a/src/Autocomplete/src/Maker/MakeAutocompleteField.php b/src/Autocomplete/src/Maker/MakeAutocompleteField.php new file mode 100644 index 00000000000..b165d087921 --- /dev/null +++ b/src/Autocomplete/src/Maker/MakeAutocompleteField.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Maker; + +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; +use Symfony\Bundle\MakerBundle\Generator; +use Symfony\Bundle\MakerBundle\InputConfiguration; +use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; +use Symfony\Bundle\MakerBundle\Str; +use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; +use Symfony\Bundle\MakerBundle\Validator; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField; +use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType; + +/** + * @author Ryan Weaver + * + * @experimental + */ +class MakeAutocompleteField extends AbstractMaker +{ + private string $className; + private string $entityClass; + + public function __construct( + private ?DoctrineHelper $doctrineHelper = null + ) { + } + + public static function getCommandName(): string + { + return 'make:autocomplete-field'; + } + + public static function getCommandDescription(): string + { + return 'Generates an Ajax-autocomplete form field class for symfony/ux-autocomplete.'; + } + + public function configureCommand(Command $command, InputConfiguration $inputConfig) + { + $command + ->setHelp(<<%command.name% command generates an Ajax-autocomplete form field class for symfony/ux-autocomplete + +php %command.full_name% + +The command will ask you which entity the field is for and what to call your new class. +EOF) + ; + } + + public function configureDependencies(DependencyBuilder $dependencies) + { + $dependencies->addClassDependency(FormInterface::class, 'symfony/form'); + } + + public function interact(InputInterface $input, ConsoleStyle $io, Command $command) + { + if (null === $this->doctrineHelper) { + throw new \LogicException('Somehow the DoctrineHelper service is missing from MakerBundle.'); + } + + $entities = $this->doctrineHelper->getEntitiesForAutocomplete(); + + $question = new Question('The class name of the entity you want to autocomplete'); + $question->setAutocompleterValues($entities); + $question->setValidator(function ($choice) use ($entities) { + return Validator::entityExists($choice, $entities); + }); + + $this->entityClass = $io->askQuestion($question); + + $defaultClass = Str::asClassName(sprintf('%s AutocompleteField', $this->entityClass)); + $this->className = $io->ask( + sprintf('Choose a name for your entity field class (e.g. %s)', $defaultClass), + $defaultClass + ); + } + + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) + { + if (null === $this->doctrineHelper) { + throw new \LogicException('Somehow the DoctrineHelper service is missing from MakerBundle.'); + } + + $entityClassDetails = $generator->createClassNameDetails( + $this->entityClass, + 'Entity\\' + ); + $entityDoctrineDetails = $this->doctrineHelper->createDoctrineDetails($entityClassDetails->getFullName()); + + $classDetails = $generator->createClassNameDetails( + $this->className, + 'Form\\', + ); + + $repositoryClassDetails = $entityDoctrineDetails->getRepositoryClass() ? $generator->createClassNameDetails('\\'.$entityDoctrineDetails->getRepositoryClass(), '') : null; + + // use App\Entity\Category; + // use App\Repository\CategoryRepository; + $useStatements = new UseStatementGenerator([ + $entityClassDetails->getFullName(), + $repositoryClassDetails ? $repositoryClassDetails->getFullName() : EntityManagerInterface::class, + AbstractType::class, + OptionsResolver::class, + AsEntityAutocompleteField::class, + ParentEntityAutocompleteType::class, + ]); + + $variables = new MakerAutocompleteVariables( + useStatements: $useStatements, + entityClassDetails: $entityClassDetails, + repositoryClassDetails: $repositoryClassDetails, + ); + $generator->generateClass( + $classDetails->getFullName(), + __DIR__.'/../Resources/skeletons/AutocompleteField.tpl.php', + [ + 'variables' => $variables, + ] + ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + + $io->text([ + 'Customize your new field class, then add it to a form:', + '', + ' $builder', + ' // ...', + sprintf(' ->add(\'%s\', %s::class)', Str::asLowerCamelCase($entityClassDetails->getShortName()), $classDetails->getShortName()), + ' ;', + ]); + } +} diff --git a/src/Autocomplete/src/Maker/MakerAutocompleteVariables.php b/src/Autocomplete/src/Maker/MakerAutocompleteVariables.php new file mode 100644 index 00000000000..a4bd24f5b74 --- /dev/null +++ b/src/Autocomplete/src/Maker/MakerAutocompleteVariables.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Maker; + +use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; +use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; + +/** + * @internal + */ +class MakerAutocompleteVariables +{ + public function __construct( + public UseStatementGenerator $useStatements, + public ClassNameDetails $entityClassDetails, + public ?ClassNameDetails $repositoryClassDetails = null, + ) { + } +} diff --git a/src/Autocomplete/src/Resources/doc/food-non-ajax.png b/src/Autocomplete/src/Resources/doc/food-non-ajax.png new file mode 100644 index 0000000000000000000000000000000000000000..08d55d92aab320eab378a145b9d6da92730b6f31 GIT binary patch literal 50176 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 +===================== + +Transform your ``EntityType``, ``ChoiceType`` or *any* ```` field won't have any autocomplete, but it *will* allow the +user to enter new options and see them as nice "items" in the box. On submit, +all of the options - separated by the ``delimiter`` - will be sent as a string. + +You *can* add autocompletion to this via the ``autocomplete_url`` option - but you'll +likely need to create your own :ref:`custom autocomplete endpoint `. + +Extending Tom Select +-------------------- + +The easiest way to customize `Tom Select`_ is via the ``tom_select_options`` +option that you pass to your field. This works great for simple +things like Tom Select's ``loadingClass`` option, which is set to a string. +But other options, like ``onInitialize``, must be set via JavaScript. + +To do this, create a custom Stimulus controller and listen to one or both +events that the core Stimulus controller dispatches: + +.. code-block:: javascript + + // assets/controllers/custom-autocomplete_controller.js + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + initialize() { + this._onPreConnect = this._onPreConnect.bind(this); + this._onConnect = this._onConnect.bind(this); + } + + connect() { + this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect); + this.element.addEventListener('autocomplete:connect', this._onConnect); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side-effects + this.element.removeEventListener('autocomplete:pre-connect', this._onConnect); + this.element.removeEventListener('autocomplete:connect', this._onPreConnect); + } + + _onPreConnect(event) { + // TomSelect has not been initialized - options can be changed + console.log(event.detail.options); // Options that will be used to initialize TomSelect + event.detail.options.onChange = (value) => { + // ... + }); + } + + _onConnect(event) { + // TomSelect has just been intialized and you can access details from the event + console.log(event.detail.tomSelect); // TomSelect instance + console.log(event.detail.options); // Options used to initialize TomSelect + } + } + +Then, update your field configuration to use your new controller (it will be used +in addition to the core Autocomplete controller): + +.. code-block:: diff + + $builder + ->add('food', EntityType::class, [ + 'class' => Food::class, + + 'attr' => [ + + 'data-controller' => 'custom-autocomplete', + + ], + ]) + +Or, if using a custom Ajax class, add the ``attr`` option to +your ``configureOptions()`` method: + +.. code-block:: diff + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'class' => Food::class, + + 'attr' => [ + + 'data-controller' => 'custom-autocomplete', + + ], + ]); + } + +.. _custom-autocompleter: + +Advanced: Creating an Autocompleter (with no Form) +-------------------------------------------------- + +If you're not using the form system, you can create an Ajax autocomplete +endpoint and then :ref:`initialize the Stimulus controller manually `. +This only works for Doctrine entities: see `Manually using the Stimulus Controller`_ +if you're autocompleting something other than an entity. + +To expose the endpoint, create a class that implements ``Symfony\\UX\\Autocomplete\\EntityAutocompleterInterface``:: + + namespace App\Autocompleter; + + use App\Entity\Food; + use Doctrine\ORM\EntityRepository; + use Doctrine\ORM\QueryBuilder; + use Symfony\Component\Security\Core\Security; + use Symfony\UX\Autocomplete\EntityAutocompleterInterface; + + class FoodAutocompleter implements EntityAutocompleterInterface + { + public function getEntityClass(): string + { + return Food::class; + } + + public function getQueryBuilder(EntityRepository $repository): QueryBuilder + { + return $repository + // the alias "food" can be anything + ->createQueryBuilder('food') + // andWhere('food.isHealthy = :isHealthy') + //->setParameter('isHealthy', true) + ; + } + + public function getLabel(object $entity): string + { + return $entity->getName(); + } + + public function getValue(object $entity): string + { + return $entity->getId(); + } + + public function getSearchableFields(): ?array + { + // see the "searchable_fields" option for details + return null; + } + + public function isGranted(Security $security): bool + { + // see the "security" option for details + return true; + } + } + +Next, tag this service with ``ux.entity_autocompleter`` and include an ``alias``: + +.. code-block:: yaml + + # config/services.yaml + services: + # ... + + App\Autocompleter\FoodAutocompleter: + tags: + - { name: ux.entity_autocompleter, alias: 'food' } + +Thanks to this, your can now autocomplete your ``Food`` entity via +the ``ux_entity_autocomplete`` route and ``alias`` route wildcard: + +.. code-block:: twig + + {{ path('ux_entity_autocomplete', { alias: 'food' }) }} + +Usually, you'll pass this URL to the Stimulus controller, which is +discussed in the next section. + +.. _manual-stimulus-controller: + +Manually using the Stimulus Controller +-------------------------------------- + +This library comes with a Stimulus controller that can activate +Tom Select on any ``select`` or ``input`` element. This can be used +outside of the Form component. For example: + +.. code-block:: twig + + + +.. _custom-autocomplete-endpoint: + +.. note:: + + If you want to create an AJAX autocomplete endpoint that is + *not* for an entity, you will need to create this manually. + The only requirement is that the response returns JSON with this format: + + .. code-block:: json + + { + "results": [ + { "value": "1", "text": "Pizza" }, + { "value": "2", "text":"Banana"} + ] + } + + Once you have this, generate the URL to your controller and + pass it to the ``url`` value of the ``stimulus_controller()`` Twig + function, or to the ``autocomplete_url`` option of your form field. + +Beyond ``url``, the Stimulus controller has various other values, +including ``tomSelectOptions``. See the `controller.ts`_ file for +the full list. + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, meaning it is not bound +to Symfony's BC policy for the moment. + +.. _`Tom Select`: https://tom-select.js.org/ +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html +.. _`Tom Select Options`: https://tom-select.js.org/docs/#general-configuration +.. _`controller.ts`: https://github.com/symfony/ux/blob/2.x/src/Autocomplete/assets/src/controller.ts +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Tom Select Render Templates`: https://tom-select.js.org/docs/#render-templates diff --git a/src/Autocomplete/src/Resources/doc/ux-autocomplete-animation.gif b/src/Autocomplete/src/Resources/doc/ux-autocomplete-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..41f6824dc81b97d5fbf40006c3aa98eb0c9a5c94 GIT binary patch literal 117888 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%0Akas+$DN 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 J#ZSEUe*jcNuwMWG literal 0 HcmV?d00001 diff --git a/src/Autocomplete/src/Resources/routes.php b/src/Autocomplete/src/Resources/routes.php new file mode 100644 index 00000000000..620d1a9fded --- /dev/null +++ b/src/Autocomplete/src/Resources/routes.php @@ -0,0 +1,9 @@ +add('ux_entity_autocomplete', '/{alias}') + ->controller('ux.autocomplete.entity_autocomplete_controller') + ; +}; diff --git a/src/Autocomplete/src/Resources/skeletons/AutocompleteField.tpl.php b/src/Autocomplete/src/Resources/skeletons/AutocompleteField.tpl.php new file mode 100644 index 00000000000..2d79c4be4a2 --- /dev/null +++ b/src/Autocomplete/src/Resources/skeletons/AutocompleteField.tpl.php @@ -0,0 +1,39 @@ + + +namespace ; + +useStatements; ?> + +#[AsEntityAutocompleteField] +class extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'class' => entityClassDetails->getShortName(); ?>::class, + 'placeholder' => 'Choose a entityClassDetails->getShortName(); ?>', + //'choice_label' => 'name', + +repositoryClassDetails) { ?> + 'query_builder' => function(repositoryClassDetails->getShortName(); ?> $repositoryClassDetails->getShortName()); ?>) { + return $repositoryClassDetails->getShortName()); ?>->createQueryBuilder('repositoryClassDetails->getShortName()); ?>'); + }, + + //'security' => 'ROLE_SOMETHING', + ]); + } + + public function getParent(): string + { + return ParentEntityAutocompleteType::class; + } +} diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ar.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ar.php new file mode 100644 index 00000000000..5622e099d79 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ar.php @@ -0,0 +1,6 @@ + 'لم يتم العثور على أي نتائج', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.bg.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.bg.php new file mode 100644 index 00000000000..236dec80f33 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.bg.php @@ -0,0 +1,6 @@ + 'Няма намерени съвпадения', + 'No more results' => 'Няма повече резултати', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ca.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ca.php new file mode 100644 index 00000000000..d968a2f8c3b --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ca.php @@ -0,0 +1,6 @@ + 'No s\'han trobat resultats', + 'No more results' => 'No hi ha més resultats', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.cs.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.cs.php new file mode 100644 index 00000000000..2869f993cef --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.cs.php @@ -0,0 +1,6 @@ + 'Nenalezeny žádné položky', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.da.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.da.php new file mode 100644 index 00000000000..66cd87bd013 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.da.php @@ -0,0 +1,6 @@ + 'Ingen resultater fundet', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.de.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.de.php new file mode 100644 index 00000000000..6396b56ca13 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.de.php @@ -0,0 +1,6 @@ + 'Keine Übereinstimmungen gefunden', + 'No more results' => 'Keine weiteren Ergebnisse', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.el.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.el.php new file mode 100644 index 00000000000..a8736633616 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.el.php @@ -0,0 +1,6 @@ + 'Δεν βρέθηκαν αποτελέσματα', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.en.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.en.php new file mode 100644 index 00000000000..af8ebb0bf97 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.en.php @@ -0,0 +1,6 @@ + 'No results found', + 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.es.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.es.php new file mode 100644 index 00000000000..05383169fdf --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.es.php @@ -0,0 +1,6 @@ + 'No se han encontrado resultados', + 'No more results' => 'No hay más resultados', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.eu.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.eu.php new file mode 100644 index 00000000000..741eb8f1370 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.eu.php @@ -0,0 +1,6 @@ + 'Ez da bat datorrenik aurkitu', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fa.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fa.php new file mode 100644 index 00000000000..12720f0b21b --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fa.php @@ -0,0 +1,6 @@ + 'هیچ نتیجه‌ای یافت نشد', + 'No more results' => 'نتیجه دیگری وجود ندارد', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fi.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fi.php new file mode 100644 index 00000000000..690808731d1 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fi.php @@ -0,0 +1,6 @@ + 'Ei tuloksia', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fr.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fr.php new file mode 100644 index 00000000000..dd8dd8b57a3 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.fr.php @@ -0,0 +1,6 @@ + 'Aucun résultat trouvé', + 'No more results' => 'Aucun autre résultat trouvé', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.gl.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.gl.php new file mode 100644 index 00000000000..50933d78937 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.gl.php @@ -0,0 +1,6 @@ + 'Non se atoparon resultados', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.hr.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.hr.php new file mode 100644 index 00000000000..5d7b76e7978 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.hr.php @@ -0,0 +1,6 @@ + 'Nema rezultata', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.hu.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.hu.php new file mode 100644 index 00000000000..18dad43a8e1 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.hu.php @@ -0,0 +1,6 @@ + 'Nincs találat', + 'No more results' => 'Nincs több találat', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.id.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.id.php new file mode 100644 index 00000000000..80f0bb9c6f1 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.id.php @@ -0,0 +1,6 @@ + 'Tidak ada hasil yang ditemukan', + 'No more results' => 'Tidak ada hasil lagi', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.it.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.it.php new file mode 100644 index 00000000000..31569cdcb76 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.it.php @@ -0,0 +1,6 @@ + 'Nessun risultato trovato', + 'No more results' => 'Non ci sono altri risultati', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.lb.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.lb.php new file mode 100644 index 00000000000..5dfc647ff09 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.lb.php @@ -0,0 +1,6 @@ + 'Keng Resultater fonnt', + 'No more results' => 'Keng weider Resultater', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.lt.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.lt.php new file mode 100644 index 00000000000..7efcf42e86e --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.lt.php @@ -0,0 +1,6 @@ + 'Atitikmenų nerasta', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.nl.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.nl.php new file mode 100644 index 00000000000..c6923c725bf --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.nl.php @@ -0,0 +1,6 @@ + 'Geen resultaten gevonden…', + 'No more results' => 'Niet meer resultaten gevonden…', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pl.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pl.php new file mode 100644 index 00000000000..44c84033cbe --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pl.php @@ -0,0 +1,6 @@ + 'Brak wyników', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt.php new file mode 100644 index 00000000000..f5476fba008 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt.php @@ -0,0 +1,6 @@ + 'Sem resultados', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt_BR.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt_BR.php new file mode 100644 index 00000000000..f968a6b898a --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.pt_BR.php @@ -0,0 +1,6 @@ + 'Nenhum resultado encontrado', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ro.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ro.php new file mode 100644 index 00000000000..9e42c79a833 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ro.php @@ -0,0 +1,6 @@ + 'Nu au fost găsite rezultate', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ru.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ru.php new file mode 100644 index 00000000000..f568b0972b4 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.ru.php @@ -0,0 +1,6 @@ + 'Совпадений не найдено', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sl.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sl.php new file mode 100644 index 00000000000..ecf851b8ecc --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sl.php @@ -0,0 +1,6 @@ + 'Ni zadetkov', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sr_RS.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sr_RS.php new file mode 100644 index 00000000000..49140df770b --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sr_RS.php @@ -0,0 +1,6 @@ + 'Nema rezultata', + 'No more results' => 'Nema više rezultata', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sv.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sv.php new file mode 100644 index 00000000000..5bc128e13d9 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.sv.php @@ -0,0 +1,6 @@ + 'Inga träffar', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.tr.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.tr.php new file mode 100644 index 00000000000..694964a3041 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.tr.php @@ -0,0 +1,6 @@ + 'Sonuç bulunamadı', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.uk.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.uk.php new file mode 100644 index 00000000000..198bda4057c --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.uk.php @@ -0,0 +1,6 @@ + 'Нічого не знайдено', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/translations/AutocompleteBundle.zh_CN.php b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.zh_CN.php new file mode 100644 index 00000000000..b57b9938cb1 --- /dev/null +++ b/src/Autocomplete/src/Resources/translations/AutocompleteBundle.zh_CN.php @@ -0,0 +1,6 @@ + '未找到结果', + // 'No more results' => 'No more results', +]; diff --git a/src/Autocomplete/src/Resources/views/autocomplete_form_theme.html.twig b/src/Autocomplete/src/Resources/views/autocomplete_form_theme.html.twig new file mode 100644 index 00000000000..d62f4470062 --- /dev/null +++ b/src/Autocomplete/src/Resources/views/autocomplete_form_theme.html.twig @@ -0,0 +1,9 @@ +{# EasyAdminAutocomplete form type #} +{% block ux_entity_autocomplete_widget %} + {{ form_widget(form.autocomplete, { attr: attr|merge({ required: required }) }) }} +{% endblock ux_entity_autocomplete_widget %} + +{% block ux_entity_autocomplete_label %} + {% set id = form.autocomplete.vars.id %} + {{ block('form_label') }} +{% endblock ux_entity_autocomplete_label %} diff --git a/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php new file mode 100644 index 00000000000..37ccb0cf85d --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Autocompleter/CustomProductAutocompleter.php @@ -0,0 +1,55 @@ +createQueryBuilder('p') + ->andWhere('p.isEnabled = :enabled') + ->setParameter('enabled', true); + } + + public function getLabel(object $entity): string + { + return $entity->getName(); + } + + public function getValue(object $entity): mixed + { + return $entity->getId(); + } + + public function getSearchableFields(): ?array + { + return ['name', 'description']; + } + + public function isGranted(Security $security): bool + { + if ($this->requestStack->getCurrentRequest()?->query->get('enforce_test_security')) { + return $security->isGranted('ROLE_USER'); + } + + return true; + } +} diff --git a/src/Autocomplete/tests/Fixtures/Entity/Category.php b/src/Autocomplete/tests/Fixtures/Entity/Category.php new file mode 100644 index 00000000000..3cc6f9b835d --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Entity/Category.php @@ -0,0 +1,75 @@ +products = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return Collection + */ + public function getProducts(): Collection + { + return $this->products; + } + + public function addProduct(Product $product): self + { + if (!$this->products->contains($product)) { + $this->products[] = $product; + $product->setCategory($this); + } + + return $this; + } + + public function removeProduct(Product $product): self + { + if ($this->products->removeElement($product)) { + // set the owning side to null (unless already changed) + if ($product->getCategory() === $this) { + $product->setCategory(null); + } + } + + return $this; + } +} diff --git a/src/Autocomplete/tests/Fixtures/Entity/Product.php b/src/Autocomplete/tests/Fixtures/Entity/Product.php new file mode 100644 index 00000000000..966ae0ece5e --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Entity/Product.php @@ -0,0 +1,97 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + public function getPrice(): ?int + { + return $this->price; + } + + public function setPrice(int $price): self + { + $this->price = $price; + + return $this; + } + + public function isEnabled(): bool + { + return $this->isEnabled; + } + + public function setIsEnabled(bool $isEnabled): self + { + $this->isEnabled = $isEnabled; + + return $this; + } + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(?Category $category): self + { + $this->category = $category; + + return $this; + } +} diff --git a/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php b/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php new file mode 100644 index 00000000000..e24abe0905d --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Factory/CategoryFactory.php @@ -0,0 +1,47 @@ + + * + * @method static Category|Proxy createOne(array $attributes = []) + * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Category|Proxy find(object|array|mixed $criteria) + * @method static Category|Proxy findOrCreate(array $attributes) + * @method static Category|Proxy first(string $sortedField = 'id') + * @method static Category|Proxy last(string $sortedField = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = [])) + * @method static Category[]|Proxy[] all() + * @method static Category[]|Proxy[] findBy(array $attributes) + * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = [])) + * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])) + * @method static EntityRepository|RepositoryProxy repository() + * @method Category|Proxy create(array|callable $attributes = []) + */ +final class CategoryFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return [ + 'name' => self::faker()->text(), + ]; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return Category::class; + } +} diff --git a/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php b/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php new file mode 100644 index 00000000000..4b99ac1f02f --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Factory/ProductFactory.php @@ -0,0 +1,55 @@ + + * + * @method static Product|Proxy createOne(array $attributes = []) + * @method static Product[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Product|Proxy find(object|array|mixed $criteria) + * @method static Product|Proxy findOrCreate(array $attributes) + * @method static Product|Proxy first(string $sortedField = 'id') + * @method static Product|Proxy last(string $sortedField = 'id') + * @method static Product|Proxy random(array $attributes = []) + * @method static Product|Proxy randomOrCreate(array $attributes = [])) + * @method static Product[]|Proxy[] all() + * @method static Product[]|Proxy[] findBy(array $attributes) + * @method static Product[]|Proxy[] randomSet(int $number, array $attributes = [])) + * @method static Product[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])) + * @method static EntityRepository|RepositoryProxy repository() + * @method Product|Proxy create(array|callable $attributes = []) + */ +final class ProductFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return [ + 'name' => self::faker()->text(), + 'category' => CategoryFactory::new(), + 'price' => self::faker()->numberBetween(1000, 9999), + 'description' => self::faker()->paragraph(), + ]; + } + + protected function initialize(): self + { + return $this; + } + + public function disable(): self + { + return $this->addState(['isEnabled' => false]); + } + + protected static function getClass(): string + { + return Product::class; + } +} diff --git a/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php b/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php new file mode 100644 index 00000000000..3a2eadaa1a4 --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php @@ -0,0 +1,48 @@ +setDefaults([ + 'class' => Category::class, + 'choice_label' => function(Category $category) { + return ''.$category->getName().''; + }, + 'query_builder' => function(EntityRepository $repository) { + return $repository->createQueryBuilder('category') + ->andWhere('category.name LIKE :search') + ->setParameter('search', '%foo%'); + }, + 'security' => function(Security $security) { + if ($this->requestStack->getCurrentRequest()?->query->get('enforce_test_security')) { + return $security->isGranted('ROLE_USER'); + } + + return true; + }, + 'placeholder' => 'What should we eat?', + ]); + } + + public function getParent(): string + { + return ParentEntityAutocompleteType::class; + } +} diff --git a/src/Autocomplete/tests/Fixtures/Form/ProductType.php b/src/Autocomplete/tests/Fixtures/Form/ProductType.php new file mode 100644 index 00000000000..9261087fd8f --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Form/ProductType.php @@ -0,0 +1,49 @@ +add('category', CategoryAutocompleteType::class) + ->add('portionSize', ChoiceType::class, [ + 'choices' => [ + 'extra small 🥨' => 'xs', + 'small' => 's', + 'medium' => 'm', + 'large' => 'l', + 'extra large' => 'xl', + 'all you can eat' => '∞', + ], + 'options_as_html' => true, + 'autocomplete' => true, + 'mapped' => false, + ]) + ->add('tags', TextType::class, [ + 'mapped' => false, + 'autocomplete' => true, + 'tom_select_options' => [ + 'create' => true, + 'createOnBlur' => true, + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Product::class, + 'csrf_protection' => false, + ]); + } +} diff --git a/src/Autocomplete/tests/Fixtures/Kernel.php b/src/Autocomplete/tests/Fixtures/Kernel.php new file mode 100644 index 00000000000..82e76b80662 --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/Kernel.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Fixtures; + +use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Bundle\MakerBundle\MakerBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Kernel as BaseKernel; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\UX\Autocomplete\AutocompleteBundle; +use Symfony\UX\Autocomplete\DependencyInjection\AutocompleteFormTypePass; +use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomProductAutocompleter; +use Symfony\UX\Autocomplete\Tests\Fixtures\Form\ProductType; +use Twig\Environment; + +final class Kernel extends BaseKernel +{ + use MicroKernelTrait; + + private bool $enableForms = true; + + public function disableForms(): void + { + $this->enableForms = false; + } + + public function testForm(FormFactoryInterface $formFactory, Environment $twig, Request $request): Response + { + $form = $formFactory->create(ProductType::class); + $form->handleRequest($request); + + return new Response($twig->render('form.html.twig', [ + 'form' => $form->createView() + ])); + } + + public function registerBundles(): iterable + { + yield new FrameworkBundle(); + yield new TwigBundle(); + yield new DoctrineBundle(); + yield new AutocompleteBundle(); + yield new SecurityBundle(); + yield new MakerBundle(); + } + + protected function configureContainer(ContainerConfigurator $c): void + { + $c->extension('framework', [ + 'secret' => 'S3CRET', + 'http_method_override' => false, + 'test' => true, + 'router' => ['utf8' => true], + 'secrets' => false, + 'session' => ['storage_factory_id' => 'session.storage.factory.mock_file'], + 'form' => ['enabled' => $this->enableForms], + ]); + + $c->extension('twig', [ + 'default_path' => '%kernel.project_dir%/tests/Fixtures/templates', + ]); + + $c->extension('doctrine', [ + 'dbal' => ['url' => '%env(resolve:DATABASE_URL)%'], + 'orm' => [ + 'auto_generate_proxy_classes' => true, + 'auto_mapping' => true, + 'mappings' => [ + 'Test' => [ + 'is_bundle' => false, + 'dir' => '%kernel.project_dir%/tests/Fixtures/Entity', + 'prefix' => 'Symfony\UX\Autocomplete\Tests\Fixtures\Entity', + 'alias' => 'Test', + ], + ], + ], + ]); + + $c->extension('security', [ + 'password_hashers' => [ + PasswordAuthenticatedUserInterface::class => 'plaintext' + ], + 'providers' => [ + 'users_in_memory' => [ + 'memory' => [ + 'users' => [ + 'mr_autocompleter' => ['password' => 'symfonypass', 'roles' => ['ROLE_USER']] + ], + ], + ] + ], + 'firewalls' => [ + 'main' => [ + 'http_basic' => true, + ], + ], + ]); + + $services = $c->services(); + $services + ->defaults() + ->autowire() + ->autoconfigure() + // disable logging errors to the console + ->set('logger', NullLogger::class) + ->load(__NAMESPACE__.'\\', __DIR__) + ->exclude(['Kernel.php']) + ; + + $services->set(CustomProductAutocompleter::class) + ->public() + ->tag(AutocompleteFormTypePass::ENTITY_AUTOCOMPLETER_TAG, [ + 'alias' => 'custom_product' + ]); + + $services->alias('public.results_executor', 'ux.autocomplete.results_executor') + ->public(); + + $services->alias('public.ux.autocomplete.make_autocomplete_field', 'ux.autocomplete.make_autocomplete_field') + ->public(); + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->import('@AutocompleteBundle/Resources/routes.php') + ->prefix('/test/autocomplete'); + + $routes->add('test_form', '/test-form')->controller('kernel::testForm'); + } +} diff --git a/src/Autocomplete/tests/Fixtures/templates/form.html.twig b/src/Autocomplete/tests/Fixtures/templates/form.html.twig new file mode 100644 index 00000000000..5159f35e65a --- /dev/null +++ b/src/Autocomplete/tests/Fixtures/templates/form.html.twig @@ -0,0 +1,5 @@ +{{ form_start(form) }} + {{ form_widget(form) }} + + +{{ form_end(form) }} diff --git a/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php new file mode 100644 index 00000000000..f03e749537b --- /dev/null +++ b/src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\CategoryFactory; +use Zenstruck\Browser\Test\HasBrowser; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +// tests CategoryAutocompleteType +class AutocompleteFormRenderingTest extends KernelTestCase +{ + use Factories; + use HasBrowser; + use ResetDatabase; + + public function testFieldsRenderWithStimulusController() + { + $this->browser() + ->throwExceptions() + ->get('/test-form') + ->assertElementAttributeContains('#product_category_autocomplete', 'data-controller', 'symfony--ux-autocomplete--autocomplete') + ->assertElementAttributeContains('#product_category_autocomplete', 'data-symfony--ux-autocomplete--autocomplete-url-value', '/test/autocomplete/category_autocomplete_type') + + ->assertElementAttributeContains('#product_portionSize', 'data-controller', 'symfony--ux-autocomplete--autocomplete') + ->assertElementAttributeContains('#product_tags', 'data-controller', 'symfony--ux-autocomplete--autocomplete') + ->assertElementAttributeContains('#product_tags', 'data-symfony--ux-autocomplete--autocomplete-tom-select-options-value', 'createOnBlur') + ; + } + + public function testCategoryFieldSubmitsCorrectly() + { + $firstCat = CategoryFactory::createOne(['name' => 'First cat']); + CategoryFactory::createOne(['name' => 'in space']); + CategoryFactory::createOne(['name' => 'ate pizza']); + + $this->browser() + ->throwExceptions() + ->get('/test-form') + // the field renders empty (but the placeholder is there) + ->assertElementCount('#product_category_autocomplete option', 1) + ->assertNotContains('First cat') + ->post('/test-form', [ + 'body' => [ + 'product' => ['category' => ['autocomplete' => $firstCat->getId()]], + ], + ]) + // the one option + placeholder now shows up + ->assertElementCount('#product_category_autocomplete option', 2) + ->assertContains('First cat') + ; + } +} diff --git a/src/Autocomplete/tests/Functional/CustomAutocompleterTest.php b/src/Autocomplete/tests/Functional/CustomAutocompleterTest.php new file mode 100644 index 00000000000..34555f80bb5 --- /dev/null +++ b/src/Autocomplete/tests/Functional/CustomAutocompleterTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\ProductFactory; +use Zenstruck\Browser\Test\HasBrowser; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +// tests CustomProductAutocompleter +class CustomAutocompleterTest extends KernelTestCase +{ + use Factories; + use HasBrowser; + use ResetDatabase; + + public function testItReturnsBasicResults(): void + { + $product = ProductFactory::createOne(['name' => 'foo']); + ProductFactory::createOne(['name' => 'bar']); + ProductFactory::createOne(['name' => 'foo and bar']); + + $this->browser() + ->throwExceptions() + ->get('/test/autocomplete/custom_product') + ->assertSuccessful() + ->assertJsonMatches('length(results)', 3) + ->assertJsonMatches('results[0].value', $product->getId()) + ->assertJsonMatches('results[0].text', 'foo') + ->get('/test/autocomplete/custom_product?query=bar') + ->assertJsonMatches('length(results)', 2) + ; + } + + public function testItUsesTheCustomQuery(): void + { + ProductFactory::createOne(['name' => 'foo']); + ProductFactory::new(['name' => 'foo and bar']) + ->disable() + ->create(); + + $this->browser() + ->throwExceptions() + ->get('/test/autocomplete/custom_product?query=foo') + ->assertSuccessful() + ->assertJsonMatches('length(results)', 1) + ->assertJsonMatches('results[0].text', 'foo') + ; + } + + public function testItOnlySearchedOnSearchableFields(): void + { + ProductFactory::createOne(['name' => 'foo', 'price' => 50]); + ProductFactory::createOne(['name' => 'bar', 'description' => 'foo 50', 'price' => 55]); + + $this->browser() + ->throwExceptions() + // search on name or description + ->get('/test/autocomplete/custom_product?query=foo') + ->assertSuccessful() + ->assertJsonMatches('length(results)', 2) + ->get('/test/autocomplete/custom_product?query=50') + // price should not be searched + ->assertJsonMatches('length(results)', 1) + ->assertJsonMatches('results[0].text', 'bar') + ; + } + + public function testItEnforcesSecurity(): void + { + ProductFactory::createMany(3); + + $this->browser() + // enforce_test_security is a custom flag used in CustomProductAutocomplete + ->get('/test/autocomplete/custom_product?enforce_test_security=1') + ->assertStatus(401) + ->actingAs(new InMemoryUser('mr_autocompleter', null, ['ROLE_USER'])) + ->get('/test/autocomplete/custom_product?enforce_test_security=1', [ + 'server' => [ + 'PHP_AUTH_USER' => 'mr_autocompleter', + 'PHP_AUTH_PW' => 'symfonypass', + ], + ]) + ->assertSuccessful() + ->assertJsonMatches('length(results)', 3) + ; + } + + public function testItReturns404OnBadAlias(): void + { + $this->browser() + ->get('/test/autocomplete/not_real') + ->assertStatus(404) + ; + } +} diff --git a/src/Autocomplete/tests/Functional/FieldAutocompleterTest.php b/src/Autocomplete/tests/Functional/FieldAutocompleterTest.php new file mode 100644 index 00000000000..1d273da0ebd --- /dev/null +++ b/src/Autocomplete/tests/Functional/FieldAutocompleterTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\CategoryFactory; +use Zenstruck\Browser\Test\HasBrowser; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +// tests CategoryAutocompleteType +class FieldAutocompleterTest extends KernelTestCase +{ + use Factories; + use HasBrowser; + use ResetDatabase; + + public function testItReturnsBasicResults(): void + { + $category = CategoryFactory::createOne(['name' => 'foo and baz']); + CategoryFactory::createOne(['name' => 'foo and bar']); + + $this->browser() + ->throwExceptions() + ->get('/test/autocomplete/category_autocomplete_type') + ->assertSuccessful() + ->assertJsonMatches('length(results)', 2) + ->assertJsonMatches('results[0].value', (string) $category->getId()) + ->assertJsonMatches('results[0].text', 'foo and baz') + ->get('/test/autocomplete/category_autocomplete_type?query=bar') + ->assertJsonMatches('length(results)', 1) + ; + } + + public function testItUsesTheCustomQuery(): void + { + CategoryFactory::createOne(['name' => 'foo and bar']); + CategoryFactory::createOne(['name' => 'baz and bar']); + + $this->browser() + ->throwExceptions() + // query already ONLY returns items matching "foo" + ->get('/test/autocomplete/category_autocomplete_type?query=bar') + ->assertSuccessful() + ->assertJsonMatches('length(results)', 1) + ->assertJsonMatches('results[0].text', 'foo and bar') + ; + } + + public function testItEnforcesSecurity(): void + { + CategoryFactory::createMany(3, [ + 'name' => 'foo so that it matches custom query', + ]); + + $this->browser() + // enforce_test_security is a custom flag used in FieldAutocompleterTest + ->get('/test/autocomplete/category_autocomplete_type?enforce_test_security=1') + ->assertStatus(401) + ->actingAs(new InMemoryUser('mr_autocompleter', null, ['ROLE_USER'])) + ->get('/test/autocomplete/category_autocomplete_type?enforce_test_security=1', [ + 'server' => [ + 'PHP_AUTH_USER' => 'mr_autocompleter', + 'PHP_AUTH_PW' => 'symfonypass', + ], + ]) + ->assertSuccessful() + ->assertJsonMatches('length(results)', 3) + ; + } +} diff --git a/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataFactoryTest.php b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataFactoryTest.php new file mode 100644 index 00000000000..c9a912f23eb --- /dev/null +++ b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataFactoryTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Integration\Doctrine; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\Autocomplete\Doctrine\EntityMetadata; +use Symfony\UX\Autocomplete\Doctrine\EntityMetadataFactory; +use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product; + +class EntityMetadataFactoryTest extends KernelTestCase +{ + public function testItSuccessfullyCreatesMetadata(): void + { + /** @var EntityMetadataFactory $factory */ + $factory = self::getContainer()->get('ux.autocomplete.entity_metadata_factory'); + $metadata = $factory->create(Product::class); + $this->assertInstanceOf(EntityMetadata::class, $metadata); + } +} diff --git a/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php new file mode 100644 index 00000000000..1dd30667edd --- /dev/null +++ b/src/Autocomplete/tests/Integration/Doctrine/EntityMetadataTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Integration\Doctrine; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\Autocomplete\Doctrine\EntityMetadata; +use Symfony\UX\Autocomplete\Doctrine\EntityMetadataFactory; +use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\ProductFactory; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +class EntityMetadataTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + public function testGetAllPropertyNames(): void + { + $this->assertSame( + ['id', 'name', 'description', 'price', 'isEnabled'], + $this->getMetadata()->getAllPropertyNames() + ); + } + + public function testIsAssociation(): void + { + $metadata = $this->getMetadata(); + $this->assertFalse($metadata->isAssociation('name')); + $this->assertTrue($metadata->isAssociation('category')); + } + + public function testGetIdValue(): void + { + $product = ProductFactory::createOne(); + $this->assertEquals($product->getId(), $this->getMetadata()->getIdValue($product->object())); + } + + public function testGetPropertyDataType(): void + { + $metadata = $this->getMetadata(); + $this->assertSame(Types::STRING, $metadata->getPropertyDataType('name')); + $this->assertEquals(ClassMetadataInfo::MANY_TO_ONE, $metadata->getPropertyDataType('category')); + } + + public function testGetPropertyMetadata(): void + { + $metadata = $this->getMetadata(); + $this->assertSame([ + 'fieldName' => 'name', + 'type' => 'string', + 'scale' => null, + 'length' => null, + 'unique' => false, + 'nullable' => false, + 'precision' => null, + 'columnName' => 'name', + ], $metadata->getPropertyMetadata('name')); + } + + public function testIsEmbeddedClassProperty(): void + { + // TODO + $this->markTestIncomplete(); + } + + private function getMetadata(): EntityMetadata + { + /** @var EntityMetadataFactory $factory */ + $factory = self::getContainer()->get('ux.autocomplete.entity_metadata_factory'); + + return $factory->create(Product::class); + } +} diff --git a/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php b/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php new file mode 100644 index 00000000000..62a29001887 --- /dev/null +++ b/src/Autocomplete/tests/Integration/Doctrine/EntitySearchUtilTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Integration\Doctrine; + +use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil; +use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Product; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\CategoryFactory; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\ProductFactory; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +class EntitySearchUtilTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + public function testItCreatesBasicStringSearchQuery(): void + { + $prod1 = ProductFactory::createOne(['name' => 'bar prod1']); + $prod2 = ProductFactory::createOne(['name' => 'foo prod2']); + ProductFactory::createOne(['name' => 'baz thing3']); + $prod4 = ProductFactory::createOne(['description' => 'all about prod 4']); + + $results = $this->callAddSearchClass('prod'); + $this->assertSame([$prod1->object(), $prod2->object(), $prod4->object()], $results); + } + + public function testItSearchesOnCorrectFields(): void + { + $prod1 = ProductFactory::createOne(['name' => 'bar prod1']); + ProductFactory::createOne(['description' => 'foo prod2']); + + $results = $this->callAddSearchClass('prod', ['name']); + $this->assertSame([$prod1->object()], $results); + } + + public function testItCanSearchOnRelationFields(): void + { + $category1 = CategoryFactory::createOne(['name' => 'foods']); + $category2 = CategoryFactory::createOne(['name' => 'toys']); + $prod1 = ProductFactory::createOne(['name' => 'pizza', 'category' => $category1]); + $prod2 = ProductFactory::createOne(['name' => 'toy food', 'category' => $category2]); + ProductFactory::createOne(['name' => 'puzzle', 'category' => $category2]); + + $results = $this->callAddSearchClass('food', ['name', 'category.name']); + $this->assertSame([$prod1->object(), $prod2->object()], $results); + } + + /** + * @return array + */ + private function callAddSearchClass(string $search, array $searchableProperties = null): array + { + /** @var ManagerRegistry $registry */ + $registry = self::getContainer()->get('doctrine'); + /** @var EntityRepository $repository */ + $repository = $registry->getRepository(Product::class); + $queryBuilder = $repository->createQueryBuilder('prod'); + + /** @var EntitySearchUtil $searchUtil */ + $searchUtil = self::getContainer()->get('ux.autocomplete.entity_search_util'); + $searchUtil->addSearchClause( + $queryBuilder, + $search, + Product::class, + $searchableProperties + ); + + return $queryBuilder->getQuery()->execute(); + } +} diff --git a/src/Autocomplete/tests/Integration/WiringTest.php b/src/Autocomplete/tests/Integration/WiringTest.php new file mode 100644 index 00000000000..c9421d6c04e --- /dev/null +++ b/src/Autocomplete/tests/Integration/WiringTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Integration; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\UX\Autocomplete\AutocompleteResultsExecutor; +use Symfony\UX\Autocomplete\Tests\Fixtures\Autocompleter\CustomProductAutocompleter; +use Symfony\UX\Autocomplete\Tests\Fixtures\Factory\ProductFactory; +use Symfony\UX\Autocomplete\Tests\Fixtures\Kernel; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +class WiringTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + protected static function createKernel(array $options = []): KernelInterface + { + $kernel = new Kernel('test', true); + $kernel->disableForms(); + + return $kernel; + } + + public function testWiringWithoutForm(): void + { + $kernel = new Kernel('test', true); + $kernel->disableForms(); + $kernel->boot(); + + ProductFactory::createMany(3); + + /** @var AutocompleteResultsExecutor $executor */ + $executor = $kernel->getContainer()->get('public.results_executor'); + $autocompleter = $kernel->getContainer()->get(CustomProductAutocompleter::class); + $this->assertCount(3, $executor->fetchResults($autocompleter, '')); + } +} diff --git a/src/Autocomplete/tests/Unit/AutocompleteResultsExecutorTest.php b/src/Autocomplete/tests/Unit/AutocompleteResultsExecutorTest.php new file mode 100644 index 00000000000..d86e7b19f43 --- /dev/null +++ b/src/Autocomplete/tests/Unit/AutocompleteResultsExecutorTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Autocomplete\Tests\Unit; + +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\Security\Security; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\UX\Autocomplete\AutocompleteResultsExecutor; +use Symfony\UX\Autocomplete\Doctrine\DoctrineRegistryWrapper; +use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil; +use Symfony\UX\Autocomplete\EntityAutocompleterInterface; + +class AutocompleteResultsExecutorTest extends TestCase +{ + public function testItExecutesTheResults() + { + $entitySearchUtil = $this->createMock(EntitySearchUtil::class); + $entitySearchUtil->expects($this->once()) + ->method('addSearchClause'); + + $doctrineRegistry = $this->createMock(DoctrineRegistryWrapper::class); + $doctrineRegistry->expects($this->any()) + ->method('getRepository') + ->willReturn($this->createMock(EntityRepository::class)); + + $queryBuilder = $this->createMock(QueryBuilder::class); + $autocompleter = $this->createMock(EntityAutocompleterInterface::class); + $autocompleter->expects($this->once()) + ->method('getQueryBuilder') + ->willReturn($queryBuilder); + $autocompleter->expects($this->exactly(2)) + ->method('getValue') + ->willReturnCallback(function (object $object) { + return $object->id; + }); + $autocompleter->expects($this->exactly(2)) + ->method('getLabel') + ->willReturnCallback(function (object $object) { + return $object->name; + }); + + $result1 = new \stdClass(); + $result1->id = 1; + $result1->name = 'Result 1'; + $result2 = new \stdClass(); + $result2->id = 2; + $result2->name = 'Result 2'; + + $mockQuery = $this->createMock(AbstractQuery::class); + $mockQuery->expects($this->once()) + ->method('execute') + ->willReturn([$result1, $result2]); + $queryBuilder->expects($this->once()) + ->method('getQuery') + ->willReturn($mockQuery); + + $executor = new AutocompleteResultsExecutor($entitySearchUtil, $doctrineRegistry); + $this->assertEquals([ + ['value' => 1, 'text' => 'Result 1'], + ['value' => 2, 'text' => 'Result 2'], + ], $executor->fetchResults($autocompleter, 'foo')); + } + + public function testItExecutesSecurity() + { + $entitySearchUtil = $this->createMock(EntitySearchUtil::class); + $doctrineRegistry = $this->createMock(DoctrineRegistryWrapper::class); + + $autocompleter = $this->createMock(EntityAutocompleterInterface::class); + $autocompleter->expects($this->once()) + ->method('isGranted') + ->willReturn(false); + + $executor = new AutocompleteResultsExecutor( + $entitySearchUtil, + $doctrineRegistry, + $this->createMock(Security::class) + ); + + $this->expectException(AccessDeniedException::class); + $executor->fetchResults($autocompleter, 'foo'); + } +} diff --git a/src/Autocomplete/tests/bootstrap.php b/src/Autocomplete/tests/bootstrap.php new file mode 100644 index 00000000000..aa69588ae53 --- /dev/null +++ b/src/Autocomplete/tests/bootstrap.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Zenstruck\Foundry\Test\TestState; + +require dirname(__DIR__).'/vendor/autoload.php'; + +TestState::disableDefaultProxyAutoRefresh(); diff --git a/src/React/Resources/assets/dist/register_controller.js b/src/React/Resources/assets/dist/register_controller.js index 564eb6128e1..eff0a3e32c0 100644 --- a/src/React/Resources/assets/dist/register_controller.js +++ b/src/React/Resources/assets/dist/register_controller.js @@ -5,7 +5,7 @@ function registerReactControllerComponents(context) { }; importAllReactComponents(context); window.resolveReactComponent = (name) => { - const component = reactControllers['./' + name + '.jsx'] || reactControllers['./' + name + '.tsx']; + const component = reactControllers[`./${name}.jsx`] || reactControllers[`./${name}.tsx`]; if (typeof component === 'undefined') { throw new Error('React controller "' + name + '" does not exist'); } diff --git a/src/React/Resources/assets/dist/render_controller.js b/src/React/Resources/assets/dist/render_controller.js index d6e5ba59e60..01721bdd02e 100644 --- a/src/React/Resources/assets/dist/render_controller.js +++ b/src/React/Resources/assets/dist/render_controller.js @@ -1,6 +1,24 @@ import React from 'react'; +import require$$0 from 'react-dom'; import { Controller } from '@hotwired/stimulus'; +var createRoot; + +var m = require$$0; +if (process.env.NODE_ENV === 'production') { + createRoot = m.createRoot; +} else { + var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + createRoot = function(c, o) { + i.usingClientEntryPoint = true; + try { + return m.createRoot(c, o); + } finally { + i.usingClientEntryPoint = false; + } + }; +} + class default_1 extends Controller { connect() { this._dispatchEvent('react:connect', { component: this.componentValue, props: this.propsValue }); @@ -13,23 +31,13 @@ class default_1 extends Controller { }); } disconnect() { - this.element.unmount(); + this.element.root.unmount(); this._dispatchEvent('react:unmount', { component: this.componentValue, props: this.propsValue }); } _renderReactElement(reactElement) { - if (parseInt(React.version) >= 18) { - const root = require('react-dom/client').createRoot(this.element); - root.render(reactElement); - this.element.unmount = () => { - root.unmount(); - }; - return; - } - const reactDom = require('react-dom'); - reactDom.render(reactElement, this.element); - this.element.unmount = () => { - reactDom.unmountComponentAtNode(this.element); - }; + const root = createRoot(this.element); + root.render(reactElement); + this.element.root = root; } _dispatchEvent(name, payload) { this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));