From c904606f4800c803216587b3d4349c84a907e754 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 15 Aug 2021 16:48:10 +0200
Subject: [PATCH 01/60] Rework for the version 2.0.x

---
 .gitattributes                                |   34 +-
 .gitignore                                    |   14 +-
 .phan/config.php                              |  344 -
 .php-cs-fixer.dist.php                        |   51 +
 .php_cs                                       |  273 -
 .unused.dist.php                              |   36 +
 .unused.php                                   |   28 -
 CHANGELOG.md                                  |    7 +-
 Makefile                                      |   24 +-
 README.md                                     |  134 +-
 alias/Aggregate.php                           |   27 -
 .../NotEnoughPropertiesException.php          |   27 -
 .../Exception/NotEnoughReducersException.php  |   27 -
 .../UnknownSortDirectionException.php         |   27 -
 alias/Aggregate/Result.php                    |   27 -
 alias/Aggregate/SortBy.php                    |   27 -
 alias/Builder.php                             |   27 -
 alias/Configuration.php                       |   27 -
 alias/Helper/DataHelper.php                   |   27 -
 alias/Helper/RedisHelper.php                  |   27 -
 alias/Index/Builder.php                       |   27 -
 alias/Index/Builder/AbstractField.php         |   27 -
 .../Exception/MissingNameException.php        |   27 -
 .../Exception/MustBeSingleCharException.php   |   27 -
 .../Index/Builder/Exception/TooManyFields.php |   27 -
 .../UnsupportedLanguageException.php          |   27 -
 alias/Index/Builder/Field.php                 |   27 -
 alias/Index/Builder/GeoField.php              |   27 -
 alias/Index/Builder/NumericField.php          |   27 -
 alias/Index/Builder/TagField.php              |   27 -
 alias/Index/Builder/TextField.php             |   27 -
 .../Exception/IndexNotFoundException.php      |   27 -
 alias/Index/InfoResult.php                    |   27 -
 alias/PartialQuery.php                        |   27 -
 .../Search/Exception/UnknownUnitException.php |   27 -
 .../UnsupportedLanguageException.php          |   27 -
 alias/Search/Filter.php                       |   27 -
 alias/Search/GeoFilter.php                    |   27 -
 alias/Search/Highlight.php                    |   27 -
 alias/Search/QueryBuilder.php                 |   27 -
 alias/Search/QueryBuilder/AndGroup.php        |   27 -
 alias/Search/QueryBuilder/ExactMatch.php      |   27 -
 alias/Search/QueryBuilder/FuzzyWord.php       |   27 -
 alias/Search/QueryBuilder/GeoFacet.php        |   27 -
 .../Search/QueryBuilder/GroupPartialQuery.php |   27 -
 alias/Search/QueryBuilder/Negation.php        |   27 -
 alias/Search/QueryBuilder/NumericFacet.php    |   27 -
 alias/Search/QueryBuilder/Optional.php        |   27 -
 alias/Search/QueryBuilder/OrGroup.php         |   27 -
 alias/Search/QueryBuilder/PartialQuery.php    |   27 -
 .../QueryBuilder/QueryExpressionAttribute.php |   27 -
 alias/Search/QueryBuilder/TagFacet.php        |   27 -
 alias/Search/QueryBuilder/TextFacet.php       |   27 -
 alias/Search/QueryBuilder/Word.php            |   27 -
 alias/Search/Result.php                       |   27 -
 alias/Search/Summarize.php                    |   27 -
 alias/Suggestion/Result.php                   |   27 -
 alias/Suggestions.php                         |   27 -
 alias/Words.php                               |   27 -
 .../Exception/NotEnoughTermsException.php     |   27 -
 alias/Words/SynonymResult.php                 |   27 -
 composer.json                                 |   36 +-
 composer.lock                                 | 6439 +++++++++++++++++
 infection.json                                |   29 -
 phpmd.xml                                     |   14 -
 phpstan.neon                                  |   16 +-
 phpunit.xml                                   |   44 -
 phpunit.xml.dist                              |   22 +
 psalm.xml                                     |   20 +-
 src/Aggregate.php                             |  282 -
 src/Aggregate/SortBy.php                      |   90 -
 .../NotEnoughFieldsException.php}             |   11 +-
 .../Exception/NotEnoughTermsException.php     |    2 +-
 ...OutOfRangeLevenshteinDistanceException.php |    2 +-
 .../Exception/UnknownUnitException.php        |    2 +-
 src/Helper/DataHelper.php                     |  162 -
 src/Helper/PaginatedResult.php                |  105 -
 src/Helper/RedisHelper.php                    |  155 -
 src/Index.php                                 |  160 +-
 src/Index/Builder.php                         |  396 -
 src/Index/Builder/AbstractField.php           |  111 -
 src/Index/Builder/Exception/TooManyFields.php |   28 -
 .../UnsupportedLanguageException.php          |   28 -
 src/Index/Builder/Field.php                   |   52 -
 src/Index/InfoResult.php                      |  248 -
 src/IndexBuilder.php                          |  323 +
 src/Pipeable.php                              |   29 -
 src/Query/Builder.php                         |  112 +
 .../Builder/AbstractGroup.php}                |   77 +-
 src/Query/Builder/AndGroup.php                |   47 +
 .../Builder/EncapsulationGroup.php}           |   50 +-
 .../Builder}/ExactMatch.php                   |   30 +-
 .../Builder/FieldFacet.php}                   |   45 +-
 .../FuzzyWord.php => Query/Builder/Fuzzy.php} |   37 +-
 src/Query/Builder/GeoFacet.php                |   48 +
 src/Query/Builder/Negation.php                |   50 +
 src/Query/Builder/NumericFacet.php            |   43 +
 .../Query/Builder/Optional.php                |   29 +-
 .../Builder/OrGroup.php}                      |   15 +-
 .../Optional.php => Query/Builder/Prefix.php} |   38 +-
 .../Builder/QueryElement.php}                 |   12 +-
 .../Builder/QueryElementAttribute.php}        |   30 +-
 .../Builder/QueryElementDecorator.php}        |    6 +-
 .../Query/Builder/QueryElementGroup.php       |    6 +-
 .../Builder/RawElement.php}                   |    6 +-
 src/Query/Builder/TagFacet.php                |   35 +
 src/Query/Builder/TextFacet.php               |   40 +
 .../QueryBuilder => Query/Builder}/Word.php   |   36 +-
 .../EscapeHelper.php => Query/Escaper.php}    |   19 +-
 src/Redis/Command/AbstractCommand.php         |  184 +
 src/Redis/Command/AddFieldOptionTrait.php     |   87 +
 src/Redis/Command/Aggregate.php               |  209 +
 .../Command/AggregateCommand/ApplyOption.php} |   20 +-
 .../AggregateCommand/GroupByOption.php        |   80 +
 .../Command/AggregateCommand/LimitOption.php} |    6 +-
 .../AggregateCommand/ReduceOption.php}        |   73 +-
 .../Command/AggregateCommand/SortByOption.php |   78 +
 .../Command/AggregateCommand/WithCursor.php   |   40 +
 src/Redis/Command/AliasAdd.php                |   63 +
 src/Redis/Command/AliasDel.php                |   55 +
 src/Redis/Command/AliasUpdate.php             |   63 +
 src/Redis/Command/Alter.php                   |   61 +
 .../Command/Config.php}                       |   56 +-
 src/Redis/Command/ConfigSet.php               |   69 +
 src/Redis/Command/Create.php                  |  202 +
 .../CreateCommandFieldOption.php              |    8 +-
 .../Command/CreateCommand/GeoFieldOption.php  |   60 +
 .../CreateCommand/NumericFieldOption.php      |   60 +
 .../Command/CreateCommand/TagFieldOption.php  |   65 +
 .../Command/CreateCommand/TextFieldOption.php |   68 +
 src/Redis/Command/CursorDel.php               |   65 +
 src/Redis/Command/CursorRead.php              |  119 +
 src/Redis/Command/DictAdd.php                 |   71 +
 src/Redis/Command/DictDel.php                 |   71 +
 src/Redis/Command/DictDump.php                |   54 +
 src/Redis/Command/DropIndex.php               |   64 +
 src/Redis/Command/Explain.php                 |   62 +
 .../Command/ExplainCli.php}                   |   11 +-
 src/Redis/Command/IndexList.php               |   35 +
 src/Redis/Command/Info.php                    |   60 +
 src/Redis/Command/LanguageOptionTrait.php     |   44 +
 .../Command/Option/AbstractCommandOption.php} |   60 +-
 .../Command/Option/CommandOption.php}         |   17 +-
 .../Command/Option/CustomValidatorOption.php  |   90 +
 .../Command/Option/DecoratedCommandOption.php |    6 +-
 .../Option/DecoratedOptionAwareTrait.php      |   37 +
 .../Command/Option/DecoratedOptionTrait.php   |   95 +
 src/Redis/Command/Option/FlagOption.php       |   76 +
 src/Redis/Command/Option/GroupedOption.php    |  134 +
 .../Command/Option/NamedOption.php}           |   29 +-
 src/Redis/Command/Option/NamelessOption.php   |   71 +
 .../Command/Option/NotEmptyOption.php}        |   67 +-
 .../Command/Option/NumberedOption.php}        |   72 +-
 .../Option/WithPublicGroupedSetterTrait.php   |   68 +
 .../Command/PaginatedCommand.php}             |   11 +-
 .../Command/PrefixFieldName.php}              |   29 +-
 src/Redis/Command/Search.php                  |  358 +
 .../Command/SearchCommand/FilterOption.php    |   76 +
 .../SearchCommand/GeoFilterOption.php}        |   55 +-
 .../Command/SearchCommand/HighlightOption.php |   55 +
 .../Command/SearchCommand/LimitOption.php}    |   74 +-
 .../SearchCommand/NumericRangeTrait.php}      |   61 +-
 .../Command/SearchCommand/SortByOption.php    |   53 +
 .../Command/SearchCommand/SummarizeOption.php |   43 +
 src/Redis/Command/SpellCheck.php              |  116 +
 .../Command/SpellCheckCommand/TermsOption.php |   70 +
 src/Redis/Command/SugAdd.php                  |   91 +
 src/Redis/Command/SugDel.php                  |   62 +
 src/Redis/Command/SugGet.php                  |  137 +
 src/Redis/Command/SugLen.php                  |   54 +
 src/Redis/Command/SynDump.php                 |   51 +
 src/Redis/Command/SynUpdate.php               |   78 +
 src/Redis/Command/TagVals.php                 |   62 +
 src/Redis/Initializer.php                     |  125 +
 .../Response/AggregateResponseItem.php}       |   11 +-
 src/Redis/Response/ArrayResponseTrait.php     |   97 +
 src/Redis/Response/CursorResponse.php         |  134 +
 src/Redis/Response/InfoResponse.php           |  242 +
 src/Redis/Response/PaginatedResponse.php      |  140 +
 .../Response/SearchResponseItem.php}          |   22 +-
 .../Response/SpellCheckResponseItem.php}      |   66 +-
 .../Response/SuggestionResponseItem.php}      |    9 +-
 src/Search.php                                |  541 --
 .../Exception/NotEnoughTermsException.php     |   26 -
 .../UnsupportedLanguageException.php          |   26 -
 src/Search/QueryBuilder.php                   |  127 -
 src/Search/QueryBuilder/GeoFacet.php          |   75 -
 src/Search/QueryBuilder/GroupPartialQuery.php |   27 -
 src/Search/QueryBuilder/TextFacet.php         |   79 -
 src/Search/Summarize.php                      |   68 -
 src/Suggestions.php                           |  176 +-
 src/Words.php                                 |  206 -
 tests/Aggregate/GroupByTest.php               |   92 -
 tests/Aggregate/ReducerTest.php               |  155 -
 tests/Aggregate/SortByTest.php                |   86 -
 tests/Helper/DataHelperTest.php               |  112 -
 tests/Helper/EscaperHelperTest.php            |  175 -
 tests/Helper/RedisHelperTest.php              |   92 -
 tests/Index/Builder/TagFieldTest.php          |   77 -
 tests/Index/Builder/TextFieldTest.php         |   94 -
 .../Query/Builder/FuzzyTest.php               |   55 +-
 tests/Query/Builder/GeoFacetTest.php          |   42 +
 tests/Query/BuilderTest.php                   |  508 ++
 tests/Redis/Command/AbstractCommandTest.php   |   66 +
 .../Redis/Command/AddFieldOptionTraitTest.php |   60 +
 tests/Redis/Command/AggregateTest.php         |  148 +
 .../AggregationCommand/ReduceOptionTest.php   |   59 +
 .../Command/AliasAddTest.php}                 |   38 +-
 .../Command/AliasDelTest.php}                 |   42 +-
 tests/Redis/Command/AliasUpdateTest.php       |   57 +
 tests/Redis/Command/AlterTest.php             |   62 +
 tests/Redis/Command/ConfigSetTest.php         |   52 +
 tests/Redis/Command/ConfigTest.php            |   72 +
 tests/Redis/Command/CreateTest.php            |  166 +
 tests/Redis/Command/CursorDelTest.php         |   52 +
 tests/Redis/Command/CursorReadTest.php        |   66 +
 tests/Redis/Command/DictAddTest.php           |   58 +
 tests/Redis/Command/DictDelTest.php           |   58 +
 tests/Redis/Command/DictDumpTest.php          |   56 +
 tests/Redis/Command/DropIndexTest.php         |   58 +
 tests/Redis/Command/ExplainCliTest.php        |   59 +
 tests/Redis/Command/ExplainTest.php           |   58 +
 tests/Redis/Command/IndexListTest.php         |   39 +
 .../Command/InfoTest.php}                     |   37 +-
 .../Option/AbstractCommandOptionTest.php      |   96 +
 .../Command/Option/BaseOptionTestTrait.php    |   50 +-
 .../Option/CustomValidatorOptionTest.php      |  122 +
 tests/Redis/Command/Option/FlagOptionTest.php |   96 +
 .../Command/Option/GroupedOptionTest.php      |  196 +
 .../Redis/Command/Option/NamedOptionTest.php  |   91 +
 .../Command/Option/NamelessOptionTest.php     |   86 +
 .../Command/Option/NotEmptyOptionTest.php     |  145 +
 .../Command/Option/NumberedOptionTest.php     |  102 +
 .../Command/Option/TestIsCompatibleTrait.php  |   36 +
 .../SearchCommand/FilterOptionTest.php        |   53 +
 .../Command/SearchCommand/LimitOptionTest.php |  101 +
 tests/Redis/Command/SearchTest.php            |  128 +
 .../SpellCheckCommand/TermsOptionTest.php     |   63 +
 tests/Redis/Command/SpellCheckTest.php        |   67 +
 tests/Redis/Command/SugAddTest.php            |   63 +
 tests/Redis/Command/SugDelTest.php            |   57 +
 tests/Redis/Command/SugGetTest.php            |   64 +
 .../Redis/Command/SugLenTest.php              |   47 +-
 tests/Redis/Command/SynDumpTest.php           |   56 +
 .../Redis/Command/SynUpdateTest.php           |   60 +-
 tests/Redis/Command/TagValsTest.php           |   57 +
 tests/Redis/InitializerTest.php               |   65 +
 tests/Redis/Response/InfoResponseTest.php     |  235 +
 tests/Search/QueryBuilder/GeoFacetTest.php    |   90 -
 .../Search/QueryBuilder/NumericFacetTest.php  |  117 -
 tests/Search/QueryBuilder/TagFacetTest.php    |   84 -
 tests/Search/QueryBuilder/TextFacetTest.php   |   85 -
 .../fixtures/Helper/EscaperHelperFixture.csv  |   32 -
 .../Redis/Command/FakeAbstractCommand.php     |   34 +-
 .../Command/FakeAddFieldOptionTraitClass1.php |   13 +-
 .../Command/FakeAddFieldOptionTraitClass2.php |    8 +-
 .../Option/FakeAbstractCommandOption.php      |   48 +
 257 files changed, 16853 insertions(+), 7402 deletions(-)
 delete mode 100644 .phan/config.php
 create mode 100644 .php-cs-fixer.dist.php
 delete mode 100644 .php_cs
 create mode 100644 .unused.dist.php
 delete mode 100644 .unused.php
 delete mode 100644 alias/Aggregate.php
 delete mode 100644 alias/Aggregate/Exception/NotEnoughPropertiesException.php
 delete mode 100644 alias/Aggregate/Exception/NotEnoughReducersException.php
 delete mode 100644 alias/Aggregate/Exception/UnknownSortDirectionException.php
 delete mode 100644 alias/Aggregate/Result.php
 delete mode 100644 alias/Aggregate/SortBy.php
 delete mode 100644 alias/Builder.php
 delete mode 100644 alias/Configuration.php
 delete mode 100644 alias/Helper/DataHelper.php
 delete mode 100644 alias/Helper/RedisHelper.php
 delete mode 100644 alias/Index/Builder.php
 delete mode 100644 alias/Index/Builder/AbstractField.php
 delete mode 100644 alias/Index/Builder/Exception/MissingNameException.php
 delete mode 100644 alias/Index/Builder/Exception/MustBeSingleCharException.php
 delete mode 100644 alias/Index/Builder/Exception/TooManyFields.php
 delete mode 100644 alias/Index/Builder/Exception/UnsupportedLanguageException.php
 delete mode 100644 alias/Index/Builder/Field.php
 delete mode 100644 alias/Index/Builder/GeoField.php
 delete mode 100644 alias/Index/Builder/NumericField.php
 delete mode 100644 alias/Index/Builder/TagField.php
 delete mode 100644 alias/Index/Builder/TextField.php
 delete mode 100644 alias/Index/Exception/IndexNotFoundException.php
 delete mode 100644 alias/Index/InfoResult.php
 delete mode 100644 alias/PartialQuery.php
 delete mode 100644 alias/Search/Exception/UnknownUnitException.php
 delete mode 100644 alias/Search/Exception/UnsupportedLanguageException.php
 delete mode 100644 alias/Search/Filter.php
 delete mode 100644 alias/Search/GeoFilter.php
 delete mode 100644 alias/Search/Highlight.php
 delete mode 100644 alias/Search/QueryBuilder.php
 delete mode 100644 alias/Search/QueryBuilder/AndGroup.php
 delete mode 100644 alias/Search/QueryBuilder/ExactMatch.php
 delete mode 100644 alias/Search/QueryBuilder/FuzzyWord.php
 delete mode 100644 alias/Search/QueryBuilder/GeoFacet.php
 delete mode 100644 alias/Search/QueryBuilder/GroupPartialQuery.php
 delete mode 100644 alias/Search/QueryBuilder/Negation.php
 delete mode 100644 alias/Search/QueryBuilder/NumericFacet.php
 delete mode 100644 alias/Search/QueryBuilder/Optional.php
 delete mode 100644 alias/Search/QueryBuilder/OrGroup.php
 delete mode 100644 alias/Search/QueryBuilder/PartialQuery.php
 delete mode 100644 alias/Search/QueryBuilder/QueryExpressionAttribute.php
 delete mode 100644 alias/Search/QueryBuilder/TagFacet.php
 delete mode 100644 alias/Search/QueryBuilder/TextFacet.php
 delete mode 100644 alias/Search/QueryBuilder/Word.php
 delete mode 100644 alias/Search/Result.php
 delete mode 100644 alias/Search/Summarize.php
 delete mode 100644 alias/Suggestion/Result.php
 delete mode 100644 alias/Suggestions.php
 delete mode 100644 alias/Words.php
 delete mode 100644 alias/Words/Exception/NotEnoughTermsException.php
 delete mode 100644 alias/Words/SynonymResult.php
 create mode 100644 composer.lock
 delete mode 100644 infection.json
 delete mode 100644 phpmd.xml
 delete mode 100644 phpunit.xml
 create mode 100644 phpunit.xml.dist
 delete mode 100644 src/Aggregate.php
 delete mode 100644 src/Aggregate/SortBy.php
 rename src/{Aggregate/Exception/NotEnoughReducersException.php => Exception/NotEnoughFieldsException.php} (83%)
 rename src/{Words => }/Exception/NotEnoughTermsException.php (96%)
 rename src/{Search => }/Exception/OutOfRangeLevenshteinDistanceException.php (97%)
 rename src/{Search => }/Exception/UnknownUnitException.php (96%)
 delete mode 100644 src/Helper/DataHelper.php
 delete mode 100644 src/Helper/PaginatedResult.php
 delete mode 100644 src/Helper/RedisHelper.php
 delete mode 100644 src/Index/Builder.php
 delete mode 100644 src/Index/Builder/AbstractField.php
 delete mode 100644 src/Index/Builder/Exception/TooManyFields.php
 delete mode 100644 src/Index/Builder/Exception/UnsupportedLanguageException.php
 delete mode 100644 src/Index/Builder/Field.php
 delete mode 100644 src/Index/InfoResult.php
 create mode 100644 src/IndexBuilder.php
 delete mode 100644 src/Pipeable.php
 create mode 100644 src/Query/Builder.php
 rename src/{Search/QueryBuilder/OrGroup.php => Query/Builder/AbstractGroup.php} (52%)
 create mode 100644 src/Query/Builder/AndGroup.php
 rename src/{Search/QueryBuilder/TagFacet.php => Query/Builder/EncapsulationGroup.php} (58%)
 rename src/{Search/QueryBuilder => Query/Builder}/ExactMatch.php (70%)
 rename src/{Search/QueryBuilder/Negation.php => Query/Builder/FieldFacet.php} (63%)
 rename src/{Search/QueryBuilder/FuzzyWord.php => Query/Builder/Fuzzy.php} (61%)
 create mode 100644 src/Query/Builder/GeoFacet.php
 create mode 100644 src/Query/Builder/Negation.php
 create mode 100644 src/Query/Builder/NumericFacet.php
 rename alias/Words/SpellingResult.php => src/Query/Builder/Optional.php (67%)
 rename src/{Index/Builder/GeoField.php => Query/Builder/OrGroup.php} (77%)
 rename src/{Search/QueryBuilder/Optional.php => Query/Builder/Prefix.php} (67%)
 rename src/{Search/QueryBuilder/PartialQuery.php => Query/Builder/QueryElement.php} (88%)
 rename src/{Search/QueryBuilder/QueryExpressionAttribute.php => Query/Builder/QueryElementAttribute.php} (77%)
 rename src/{Search/Exception/SyntaxErrorException.php => Query/Builder/QueryElementDecorator.php} (90%)
 rename alias/Index.php => src/Query/Builder/QueryElementGroup.php (88%)
 rename src/{Search/QueryBuilder/RawExpression.php => Query/Builder/RawElement.php} (90%)
 create mode 100644 src/Query/Builder/TagFacet.php
 create mode 100644 src/Query/Builder/TextFacet.php
 rename src/{Search/QueryBuilder => Query/Builder}/Word.php (70%)
 rename src/{Helper/EscapeHelper.php => Query/Escaper.php} (90%)
 create mode 100644 src/Redis/Command/AbstractCommand.php
 create mode 100644 src/Redis/Command/AddFieldOptionTrait.php
 create mode 100644 src/Redis/Command/Aggregate.php
 rename src/{Aggregate/Exception/NotEnoughPropertiesException.php => Redis/Command/AggregateCommand/ApplyOption.php} (71%)
 create mode 100644 src/Redis/Command/AggregateCommand/GroupByOption.php
 rename src/{Index/Builder/Exception/MissingNameException.php => Redis/Command/AggregateCommand/LimitOption.php} (88%)
 rename src/{Aggregate/Reducer.php => Redis/Command/AggregateCommand/ReduceOption.php} (61%)
 create mode 100644 src/Redis/Command/AggregateCommand/SortByOption.php
 create mode 100644 src/Redis/Command/AggregateCommand/WithCursor.php
 create mode 100644 src/Redis/Command/AliasAdd.php
 create mode 100644 src/Redis/Command/AliasDel.php
 create mode 100644 src/Redis/Command/AliasUpdate.php
 create mode 100644 src/Redis/Command/Alter.php
 rename src/{Index/Builder/TextField.php => Redis/Command/Config.php} (51%)
 create mode 100644 src/Redis/Command/ConfigSet.php
 create mode 100644 src/Redis/Command/Create.php
 rename alias/Aggregate/GroupBy.php => src/Redis/Command/CreateCommand/CreateCommandFieldOption.php (83%)
 create mode 100644 src/Redis/Command/CreateCommand/GeoFieldOption.php
 create mode 100644 src/Redis/Command/CreateCommand/NumericFieldOption.php
 create mode 100644 src/Redis/Command/CreateCommand/TagFieldOption.php
 create mode 100644 src/Redis/Command/CreateCommand/TextFieldOption.php
 create mode 100644 src/Redis/Command/CursorDel.php
 create mode 100644 src/Redis/Command/CursorRead.php
 create mode 100644 src/Redis/Command/DictAdd.php
 create mode 100644 src/Redis/Command/DictDel.php
 create mode 100644 src/Redis/Command/DictDump.php
 create mode 100644 src/Redis/Command/DropIndex.php
 create mode 100644 src/Redis/Command/Explain.php
 rename src/{Index/Builder/NumericField.php => Redis/Command/ExplainCli.php} (85%)
 create mode 100644 src/Redis/Command/IndexList.php
 create mode 100644 src/Redis/Command/Info.php
 create mode 100644 src/Redis/Command/LanguageOptionTrait.php
 rename src/{Search/Highlight.php => Redis/Command/Option/AbstractCommandOption.php} (51%)
 rename src/{PartialQuery.php => Redis/Command/Option/CommandOption.php} (76%)
 create mode 100644 src/Redis/Command/Option/CustomValidatorOption.php
 rename alias/Search.php => src/Redis/Command/Option/DecoratedCommandOption.php (87%)
 create mode 100644 src/Redis/Command/Option/DecoratedOptionAwareTrait.php
 create mode 100644 src/Redis/Command/Option/DecoratedOptionTrait.php
 create mode 100644 src/Redis/Command/Option/FlagOption.php
 create mode 100644 src/Redis/Command/Option/GroupedOption.php
 rename src/{Index/Exception/IndexNotFoundException.php => Redis/Command/Option/NamedOption.php} (69%)
 create mode 100644 src/Redis/Command/Option/NamelessOption.php
 rename src/{Search/QueryBuilder/AndGroup.php => Redis/Command/Option/NotEmptyOption.php} (51%)
 rename src/{Helper/PipelineItem.php => Redis/Command/Option/NumberedOption.php} (50%)
 create mode 100644 src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
 rename src/{Index/Builder/Exception/MustBeSingleCharException.php => Redis/Command/PaginatedCommand.php} (80%)
 rename src/{Aggregate/Exception/UnknownSortDirectionException.php => Redis/Command/PrefixFieldName.php} (68%)
 create mode 100644 src/Redis/Command/Search.php
 create mode 100644 src/Redis/Command/SearchCommand/FilterOption.php
 rename src/{Search/GeoFilter.php => Redis/Command/SearchCommand/GeoFilterOption.php} (54%)
 create mode 100644 src/Redis/Command/SearchCommand/HighlightOption.php
 rename src/{Pipeline.php => Redis/Command/SearchCommand/LimitOption.php} (50%)
 rename src/{Search/QueryBuilder/NumericFacet.php => Redis/Command/SearchCommand/NumericRangeTrait.php} (59%)
 create mode 100644 src/Redis/Command/SearchCommand/SortByOption.php
 create mode 100644 src/Redis/Command/SearchCommand/SummarizeOption.php
 create mode 100644 src/Redis/Command/SpellCheck.php
 create mode 100644 src/Redis/Command/SpellCheckCommand/TermsOption.php
 create mode 100644 src/Redis/Command/SugAdd.php
 create mode 100644 src/Redis/Command/SugDel.php
 create mode 100644 src/Redis/Command/SugGet.php
 create mode 100644 src/Redis/Command/SugLen.php
 create mode 100644 src/Redis/Command/SynDump.php
 create mode 100644 src/Redis/Command/SynUpdate.php
 create mode 100644 src/Redis/Command/TagVals.php
 create mode 100644 src/Redis/Initializer.php
 rename src/{Aggregate/Result.php => Redis/Response/AggregateResponseItem.php} (93%)
 create mode 100644 src/Redis/Response/ArrayResponseTrait.php
 create mode 100644 src/Redis/Response/CursorResponse.php
 create mode 100644 src/Redis/Response/InfoResponse.php
 create mode 100644 src/Redis/Response/PaginatedResponse.php
 rename src/{Search/Result.php => Redis/Response/SearchResponseItem.php} (85%)
 rename src/{Words/SpellingResult.php => Redis/Response/SpellCheckResponseItem.php} (55%)
 rename src/{Suggestion/Result.php => Redis/Response/SuggestionResponseItem.php} (93%)
 delete mode 100644 src/Search.php
 delete mode 100644 src/Search/Exception/NotEnoughTermsException.php
 delete mode 100644 src/Search/Exception/UnsupportedLanguageException.php
 delete mode 100644 src/Search/QueryBuilder.php
 delete mode 100644 src/Search/QueryBuilder/GeoFacet.php
 delete mode 100644 src/Search/QueryBuilder/GroupPartialQuery.php
 delete mode 100644 src/Search/QueryBuilder/TextFacet.php
 delete mode 100644 src/Search/Summarize.php
 delete mode 100644 src/Words.php
 delete mode 100644 tests/Aggregate/GroupByTest.php
 delete mode 100644 tests/Aggregate/ReducerTest.php
 delete mode 100644 tests/Aggregate/SortByTest.php
 delete mode 100644 tests/Helper/DataHelperTest.php
 delete mode 100644 tests/Helper/EscaperHelperTest.php
 delete mode 100644 tests/Helper/RedisHelperTest.php
 delete mode 100644 tests/Index/Builder/TagFieldTest.php
 delete mode 100644 tests/Index/Builder/TextFieldTest.php
 rename src/Aggregate/GroupBy.php => tests/Query/Builder/FuzzyTest.php (52%)
 create mode 100644 tests/Query/Builder/GeoFacetTest.php
 create mode 100644 tests/Query/BuilderTest.php
 create mode 100644 tests/Redis/Command/AbstractCommandTest.php
 create mode 100644 tests/Redis/Command/AddFieldOptionTraitTest.php
 create mode 100644 tests/Redis/Command/AggregateTest.php
 create mode 100644 tests/Redis/Command/AggregationCommand/ReduceOptionTest.php
 rename tests/{Index/Builder/NumericFieldTest.php => Redis/Command/AliasAddTest.php} (59%)
 rename tests/{support/Assertion.php => Redis/Command/AliasDelTest.php} (58%)
 create mode 100644 tests/Redis/Command/AliasUpdateTest.php
 create mode 100644 tests/Redis/Command/AlterTest.php
 create mode 100644 tests/Redis/Command/ConfigSetTest.php
 create mode 100644 tests/Redis/Command/ConfigTest.php
 create mode 100644 tests/Redis/Command/CreateTest.php
 create mode 100644 tests/Redis/Command/CursorDelTest.php
 create mode 100644 tests/Redis/Command/CursorReadTest.php
 create mode 100644 tests/Redis/Command/DictAddTest.php
 create mode 100644 tests/Redis/Command/DictDelTest.php
 create mode 100644 tests/Redis/Command/DictDumpTest.php
 create mode 100644 tests/Redis/Command/DropIndexTest.php
 create mode 100644 tests/Redis/Command/ExplainCliTest.php
 create mode 100644 tests/Redis/Command/ExplainTest.php
 create mode 100644 tests/Redis/Command/IndexListTest.php
 rename tests/{Index/Builder/GeoFieldTest.php => Redis/Command/InfoTest.php} (61%)
 create mode 100644 tests/Redis/Command/Option/AbstractCommandOptionTest.php
 rename src/Configuration.php => tests/Redis/Command/Option/BaseOptionTestTrait.php (55%)
 create mode 100644 tests/Redis/Command/Option/CustomValidatorOptionTest.php
 create mode 100644 tests/Redis/Command/Option/FlagOptionTest.php
 create mode 100644 tests/Redis/Command/Option/GroupedOptionTest.php
 create mode 100644 tests/Redis/Command/Option/NamedOptionTest.php
 create mode 100644 tests/Redis/Command/Option/NamelessOptionTest.php
 create mode 100644 tests/Redis/Command/Option/NotEmptyOptionTest.php
 create mode 100644 tests/Redis/Command/Option/NumberedOptionTest.php
 create mode 100644 tests/Redis/Command/Option/TestIsCompatibleTrait.php
 create mode 100644 tests/Redis/Command/SearchCommand/FilterOptionTest.php
 create mode 100644 tests/Redis/Command/SearchCommand/LimitOptionTest.php
 create mode 100644 tests/Redis/Command/SearchTest.php
 create mode 100644 tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php
 create mode 100644 tests/Redis/Command/SpellCheckTest.php
 create mode 100644 tests/Redis/Command/SugAddTest.php
 create mode 100644 tests/Redis/Command/SugDelTest.php
 create mode 100644 tests/Redis/Command/SugGetTest.php
 rename src/Search/Filter.php => tests/Redis/Command/SugLenTest.php (59%)
 create mode 100644 tests/Redis/Command/SynDumpTest.php
 rename src/Index/Builder/TagField.php => tests/Redis/Command/SynUpdateTest.php (52%)
 create mode 100644 tests/Redis/Command/TagValsTest.php
 create mode 100644 tests/Redis/InitializerTest.php
 create mode 100644 tests/Redis/Response/InfoResponseTest.php
 delete mode 100644 tests/Search/QueryBuilder/GeoFacetTest.php
 delete mode 100644 tests/Search/QueryBuilder/NumericFacetTest.php
 delete mode 100644 tests/Search/QueryBuilder/TagFacetTest.php
 delete mode 100644 tests/Search/QueryBuilder/TextFacetTest.php
 delete mode 100644 tests/fixtures/Helper/EscaperHelperFixture.csv
 rename src/Words/SynonymResult.php => tests/fixtures/Redis/Command/FakeAbstractCommand.php (70%)
 rename src/Builder.php => tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass1.php (86%)
 rename alias/Aggregate/Reducer.php => tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass2.php (84%)
 create mode 100644 tests/fixtures/Redis/Command/Option/FakeAbstractCommandOption.php

diff --git a/.gitattributes b/.gitattributes
index 9dfeb39..eab2151 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,30 +1,26 @@
-*         text=auto
-*.md      text diff=markdown
-*.json    text
-*.toml    text
-*.xml     text
-*.yaml    text
-*.yml     text
-*.php     text diff=php
-.php_cs   text diff=php
-*.lock    text -diff
-Makefile  text
+*               text=auto
+
+*.*ignore       text
+.gitattributes  text
+*.lock          text -diff
+Makefile        text
+*.md            text diff=markdown
+*.neon          text
+*.php           text diff=php
+*.xml           text
+*.xml.dist      text
 
 #
 # Exclude files from exporting
 #
-
-/.phan            export-ignore
 /tests            export-ignore
 /.gitattributes   export-ignore
 /.gitignore       export-ignore
-/.php_cs          export-ignore
-/.unused.php      export-ignore
+/.gitkeep         export-ignore
 /CHANGELOG.md     export-ignore
 /CONTRIBUTING.md  export-ignore
-/infection.json   export-ignore
 /Makefile         export-ignore
-/phpmd.xml        export-ignore
+/*.dist.php       export-ignore
+/*.xml.dist       export-ignore
 /phpstan.neon     export-ignore
-/phpunit.xml      export-ignore
-/psalm.xml        export-ignore
\ No newline at end of file
+/psalm.xml        export-ignore
diff --git a/.gitignore b/.gitignore
index 93bf08c..7e3383a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,16 @@
+### Composer
 /composer.phar
 /vendor/
 /composer.lock
 
-/.php_cs.cache
-/.phpunit.cache/
+### PHP CS Fixer
+/.php-cs-fixer.cache
+
+### PHPUnit
 /.phpunit.result.cache
-/infection.log
+
+### Documentation
+/docs
+
+### Local test
+/test*.php
\ No newline at end of file
diff --git a/.phan/config.php b/.phan/config.php
deleted file mode 100644
index 5ab5135..0000000
--- a/.phan/config.php
+++ /dev/null
@@ -1,344 +0,0 @@
-<?php
-
-use Phan\Issue;
-
-/**
- * This configuration file was automatically generated by 'phan --init --init-level=1'
- *
- * TODOs (added by 'phan --init'):
- *
- * - Go through this file and verify that there are no missing/unnecessary files/directories.
- *   (E.g. this only includes direct composer dependencies - You may have to manually add indirect composer dependencies to 'directory_list')
- * - Look at 'plugins' and add or remove plugins if appropriate (see https://github.com/phan/phan/tree/master/.phan/plugins#plugins)
- * - Add global suppressions for pre-existing issues to suppress_issue_types (https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base)
- *
- * This configuration will be read and overlaid on top of the
- * default configuration. Command line arguments will be applied
- * after this file is read.
- *
- * @see src/Phan/Config.php
- * See Config for all configurable options.
- *
- * A Note About Paths
- * ==================
- *
- * Files referenced from this file should be defined as
- *
- * ```
- *   Config::projectPath('relative_path/to/file')
- * ```
- *
- * where the relative path is relative to the root of the
- * project which is defined as either the working directory
- * of the phan executable or a path passed in via the CLI
- * '-d' flag.
- */
-return [
-
-    // Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`.
-    // If this is set to `null`,
-    // then Phan assumes the PHP version which is closest to the minor version
-    // of the php executable used to execute Phan.
-    //
-    // Note that the **only** effect of choosing `'5.6'` is to infer that functions removed in php 7.0 exist.
-    // (See `backward_compatibility_checks` for additional options)
-    'target_php_version' => '7.1',
-
-    // If enabled, missing properties will be created when
-    // they are first seen. If false, we'll report an
-    // error message if there is an attempt to write
-    // to a class property that wasn't explicitly
-    // defined.
-    'allow_missing_properties' => false,
-
-    // If enabled, null can be cast to any type and any
-    // type can be cast to null. Setting this to true
-    // will cut down on false positives.
-    'null_casts_as_any_type' => false,
-
-    // If enabled, allow null to be cast as any array-like type.
-    //
-    // This is an incremental step in migrating away from `null_casts_as_any_type`.
-    // If `null_casts_as_any_type` is true, this has no effect.
-    'null_casts_as_array' => false,
-
-    // If enabled, allow any array-like type to be cast to null.
-    // This is an incremental step in migrating away from `null_casts_as_any_type`.
-    // If `null_casts_as_any_type` is true, this has no effect.
-    'array_casts_as_null' => false,
-
-    // If enabled, scalars (int, float, bool, string, null)
-    // are treated as if they can cast to each other.
-    // This does not affect checks of array keys. See `scalar_array_key_cast`.
-    'scalar_implicit_cast' => false,
-
-    // If enabled, any scalar array keys (int, string)
-    // are treated as if they can cast to each other.
-    // E.g. `array<int,stdClass>` can cast to `array<string,stdClass>` and vice versa.
-    // Normally, a scalar type such as int could only cast to/from int and mixed.
-    'scalar_array_key_cast' => false,
-
-    // If this has entries, scalars (int, float, bool, string, null)
-    // are allowed to perform the casts listed.
-    //
-    // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]`
-    // allows casting null to a string, but not vice versa.
-    // (subset of `scalar_implicit_cast`)
-    'scalar_implicit_partial' => [],
-
-    // If enabled, Phan will warn if **any** type in a method invocation's object
-    // is definitely not an object,
-    // or if **any** type in an invoked expression is not a callable.
-    // Setting this to true will introduce numerous false positives
-    // (and reveal some bugs).
-    'strict_method_checking' => true,
-
-    // If enabled, Phan will warn if **any** type of the object expression for a property access
-    // does not contain that property.
-    'strict_object_checking' => true,
-
-    // If enabled, Phan will warn if **any** type in the argument's union type
-    // cannot be cast to a type in the parameter's expected union type.
-    // Setting this to true will introduce numerous false positives
-    // (and reveal some bugs).
-    'strict_param_checking' => true,
-
-    // If enabled, Phan will warn if **any** type in a property assignment's union type
-    // cannot be cast to a type in the property's declared union type.
-    // Setting this to true will introduce numerous false positives
-    // (and reveal some bugs).
-    'strict_property_checking' => true,
-
-    // If enabled, Phan will warn if **any** type in a returned value's union type
-    // cannot be cast to the declared return type.
-    // Setting this to true will introduce numerous false positives
-    // (and reveal some bugs).
-    'strict_return_checking' => true,
-
-    // If true, seemingly undeclared variables in the global
-    // scope will be ignored.
-    //
-    // This is useful for projects with complicated cross-file
-    // globals that you have no hope of fixing.
-    'ignore_undeclared_variables_in_global_scope' => false,
-
-    // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for,
-    // but aren't available in the codebase, or from Reflection.
-    // (may lead to false positives if an extension isn't loaded)
-    //
-    // If this is true(default), then Phan will not warn.
-    //
-    // Even when this is false, Phan will still infer return values and check parameters of internal functions
-    // if Phan has the signatures.
-    'ignore_undeclared_functions_with_known_signatures' => false,
-
-    // Backwards Compatibility Checking. This is slow
-    // and expensive, but you should consider running
-    // it before upgrading your version of PHP to a
-    // new version that has backward compatibility
-    // breaks.
-    //
-    // If you are migrating from PHP 5 to PHP 7,
-    // you should also look into using
-    // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc)
-    // and [php7mar](https://github.com/Alexia/php7mar),
-    // which have different backwards compatibility checks.
-    'backward_compatibility_checks' => false,
-
-    // If true, check to make sure the return type declared
-    // in the doc-block (if any) matches the return type
-    // declared in the method signature.
-    'check_docblock_signature_return_type_match' => true,
-
-    // This setting maps case-insensitive strings to union types.
-    //
-    // This is useful if a project uses phpdoc that differs from the phpdoc2 standard.
-    //
-    // If the corresponding value is the empty string,
-    // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
-    //
-    // If the corresponding value is not empty,
-    // then Phan will act as though it saw the corresponding UnionTypes(s)
-    // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc.
-    //
-    // This matches the **entire string**, not parts of the string.
-    // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting)
-    //
-    // (These are not aliases, this setting is ignored outside of doc comments).
-    // (Phan does not check if classes with these names exist)
-    //
-    // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']`
-    'phpdoc_type_mapping' => [],
-
-    // Set to true in order to attempt to detect dead
-    // (unreferenced) code. Keep in mind that the
-    // results will only be a guess given that classes,
-    // properties, constants and methods can be referenced
-    // as variables (like `$class->$property` or
-    // `$class->$method()`) in ways that we're unable
-    // to make sense of.
-    'dead_code_detection' => false,
-
-    // Set to true in order to attempt to detect unused variables.
-    // `dead_code_detection` will also enable unused variable detection.
-    //
-    // This has a few known false positives, e.g. for loops or branches.
-    'unused_variable_detection' => true,
-
-    // Set to true in order to attempt to detect redundant and impossible conditions.
-    //
-    // This has some false positives involving loops,
-    // variables set in branches of loops, and global variables.
-    'redundant_condition_detection' => true,
-
-    // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions,
-    // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version).
-    //
-    // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect.
-    // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x.
-    'assume_real_types_for_internal_functions' => true,
-
-    // If true, this runs a quick version of checks that takes less
-    // time at the cost of not running as thorough
-    // of an analysis. You should consider setting this
-    // to true only when you wish you had more **undiagnosed** issues
-    // to fix in your code base.
-    //
-    // In quick-mode the scanner doesn't rescan a function
-    // or a method's code block every time a call is seen.
-    // This means that the problem here won't be detected:
-    //
-    // ```php
-    // <?php
-    // function test($arg):int {
-    //     return $arg;
-    // }
-    // test("abc");
-    // ```
-    //
-    // This would normally generate:
-    //
-    // ```
-    // test.php:3 PhanTypeMismatchReturn Returning type string but test() is declared to return int
-    // ```
-    //
-    // The initial scan of the function's code block has no
-    // type information for `$arg`. It isn't until we see
-    // the call and rescan `test()`'s code block that we can
-    // detect that it is actually returning the passed in
-    // `string` instead of an `int` as declared.
-    'quick_mode' => false,
-
-    // Override to hardcode existence and types of (non-builtin) globals in the global scope.
-    // Class names should be prefixed with `\`.
-    //
-    // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`)
-    'globals_type_map' => [],
-
-    // The minimum severity level to report on. This can be
-    // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or
-    // `Issue::SEVERITY_CRITICAL`. Setting it to only
-    // critical issues is a good place to start on a big
-    // sloppy mature code base.
-    'minimum_severity' => Issue::SEVERITY_LOW,
-
-    // Add any issue types (such as `'PhanUndeclaredMethod'`)
-    // to this black-list to inhibit them from being reported.
-    'suppress_issue_types' => [],
-
-    // A regular expression to match files to be excluded
-    // from parsing and analysis and will not be read at all.
-    //
-    // This is useful for excluding groups of test or example
-    // directories/files, unanalyzable files, or files that
-    // can't be removed for whatever reason.
-    // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`)
-    'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
-
-    // A list of files that will be excluded from parsing and analysis
-    // and will not be read at all.
-    //
-    // This is useful for excluding hopelessly unanalyzable
-    // files that can't be removed for whatever reason.
-    'exclude_file_list' => [],
-
-    // A directory list that defines files that will be excluded
-    // from static analysis, but whose class and method
-    // information should be included.
-    //
-    // Generally, you'll want to include the directories for
-    // third-party code (such as "vendor/") in this list.
-    //
-    // n.b.: If you'd like to parse but not analyze 3rd
-    //       party code, directories containing that code
-    //       should be added to the `directory_list` as well as
-    //       to `exclude_analysis_directory_list`.
-    'exclude_analysis_directory_list' => [
-        'vendor/',
-    ],
-
-    // Enable this to enable checks of require/include statements referring to valid paths.
-    // The settings `include_paths` and `warn_about_relative_include_statement` affect the checks.
-    'enable_include_path_checks' => true,
-
-    // The number of processes to fork off during the analysis
-    // phase.
-    'processes' => 1,
-
-    // List of case-insensitive file extensions supported by Phan.
-    // (e.g. `['php', 'html', 'htm']`)
-    'analyzed_file_extensions' => [
-        'php',
-    ],
-
-    // You can put paths to stubs of internal extensions in this config option.
-    // If the corresponding extension is **not** loaded, then Phan will use the stubs instead.
-    // Phan will continue using its detailed type annotations,
-    // but load the constants, classes, functions, and classes (and their Reflection types)
-    // from these stub files (doubling as valid php files).
-    // Use a different extension from php to avoid accidentally loading these.
-    // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now)
-    //
-    // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`)
-    'autoload_internal_extension_signatures' => [],
-
-    // A list of plugin files to execute.
-    //
-    // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`)
-    //
-    // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins).
-    //
-    // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`)
-    'plugins' => [
-        'AlwaysReturnPlugin',
-        'DollarDollarPlugin',
-        'DuplicateArrayKeyPlugin',
-        'DuplicateExpressionPlugin',
-        'PregRegexCheckerPlugin',
-        'PrintfCheckerPlugin',
-        'SleepCheckerPlugin',
-        'UnreachableCodePlugin',
-        'UseReturnValuePlugin',
-        'EmptyStatementListPlugin',
-        'StrictComparisonPlugin',
-        'LoopVariableReusePlugin',
-    ],
-
-    // A list of directories that should be parsed for class and
-    // method information. After excluding the directories
-    // defined in `exclude_analysis_directory_list`, the remaining
-    // files will be statically analyzed for errors.
-    //
-    // Thus, both first-party and third-party code being used by
-    // your application should be included in this list.
-    'directory_list' => [
-        'src',
-        'vendor/predis/predis',
-        'vendor/sgh/comparable/src'
-    ],
-
-    // A list of individual files to include in analysis
-    // with a path relative to the root directory of the
-    // project.
-    'file_list' => [],
-];
\ No newline at end of file
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..c6d01b1
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,51 @@
+<?php
+/*
+ * This document has been generated with
+ * https://mlocati.github.io/php-cs-fixer-configurator/#version:3.0.0|configurator
+ * you can change this configuration by importing this file.
+ */
+$config = new PhpCsFixer\Config();
+return $config
+    ->setRiskyAllowed(true)
+    ->setRules([
+        '@PHP54Migration' => true,
+        '@PHP70Migration' => true,
+        '@PHP71Migration' => true,
+        '@PHP73Migration' => true,
+        '@PSR1' => true,
+        '@PSR12' => true,
+        '@PhpCsFixer' => true,
+        'combine_nested_dirname' => true,
+        'dir_constant' => true,
+        'global_namespace_import' => ['import_functions'=>true],
+        'header_comment' => ['comment_type'=>'comment','header'=>"Copyright MacFJA\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\nrights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the\nSoftware.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\nWARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",'location'=>'after_declare_strict'],
+        'implode_call' => true,
+        'logical_operators' => true,
+        'modernize_types_casting' => true,
+        'native_function_invocation' => true,
+        'no_alias_functions' => ['sets'=>['@all']],
+        'no_php4_constructor' => true,
+        'no_superfluous_phpdoc_tags' => ['allow_mixed'=>true,'remove_inheritdoc'=>true],
+        'no_unreachable_default_argument_value' => true,
+        'no_useless_sprintf' => true,
+        'php_unit_dedicate_assert' => true,
+        'php_unit_dedicate_assert_internal_type' => true,
+        'php_unit_expectation' => true,
+        'php_unit_mock' => true,
+        'php_unit_mock_short_will_return' => true,
+        'php_unit_namespaced' => true,
+        'php_unit_set_up_tear_down_visibility' => true,
+        'php_unit_test_case_static_method_calls' => true,
+        'self_static_accessor' => true,
+        'set_type_to_cast' => true,
+        'static_lambda' => true,
+        'declare_strict_types' => true,
+        'ternary_to_elvis_operator' => true,
+        'void_return' => true,
+    ])
+    ->setFinder(PhpCsFixer\Finder::create()
+        ->exclude('vendor')
+        ->in(__DIR__.'/src')
+        ->in(__DIR__.'/tests')
+    )
+    ;
\ No newline at end of file
diff --git a/.php_cs b/.php_cs
deleted file mode 100644
index 9f35830..0000000
--- a/.php_cs
+++ /dev/null
@@ -1,273 +0,0 @@
-<?php
-/*
- * This document has been generated with
- * https://mlocati.github.io/php-cs-fixer-configurator/#version:2.16.3|configurator
- * you can change this configuration by importing this file.
- */
-return PhpCsFixer\Config::create()
-    ->setRiskyAllowed(true)
-    ->setRules([
-        '@PhpCsFixer' => true,
-        '@PSR1' => true,
-        '@PSR2' => true,
-        '@PHP70Migration' => true,
-        '@PHP71Migration' => true,
-        // Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one.
-        'align_multiline_comment' => true,
-        // Each element of an array must be indented exactly once.
-        'array_indentation' => true,
-        // PHP arrays should be declared using the configured syntax.
-        'array_syntax' => ['syntax'=>'short'],
-        // Binary operators should be surrounded by space as configured.
-        'binary_operator_spaces' => true,
-        // There MUST be one blank line after the namespace declaration.
-        'blank_line_after_namespace' => true,
-        // Ensure there is no code on the same line as the PHP open tag and it is followed by a blank line.
-        'blank_line_after_opening_tag' => true,
-        // An empty line feed must precede any configured statement.
-        'blank_line_before_statement' => true,
-        // The body of each structure MUST be enclosed by braces. Braces should be properly placed. Body of braces should be properly indented.
-        'braces' => true,
-        // A single space or none should be between cast and variable.
-        'cast_spaces' => true,
-        // Class, trait and interface elements must be separated with one blank line.
-        'class_attributes_separation' => true,
-        // Whitespace around the keywords of a class, trait or interfaces definition should be one space.
-        'class_definition' => true,
-        // Using `isset($var) &&` multiple times should be done in one call.
-        'combine_consecutive_issets' => false,
-        // Calling `unset` on multiple items should be done in one call.
-        'combine_consecutive_unsets' => false,
-        // Remove extra spaces in a nullable typehint.
-        'compact_nullable_typehint' => true,
-        // Concatenation should be spaced according configuration.
-        'concat_space' => true,
-        // The PHP constants `true`, `false`, and `null` MUST be written using the correct casing.
-        'constant_case' => true,
-        // Equal sign in declare statement should be surrounded by spaces or not following configuration.
-        'declare_equal_normalize' => true,
-        // The keyword `elseif` should be used instead of `else if` so that all control keywords look like single words.
-        'elseif' => true,
-        // PHP code MUST use only UTF-8 without BOM (remove BOM).
-        'encoding' => true,
-        // Escape implicit backslashes in strings and heredocs to ease the understanding of which are special chars interpreted by PHP and which not.
-        'escape_implicit_backslashes' => true,
-        // Add curly braces to indirect variables to make them clear to understand. Requires PHP >= 7.0.
-        'explicit_indirect_variable' => true,
-        // Converts implicit variables into explicit ones in double-quoted strings or heredoc syntax.
-        'explicit_string_variable' => true,
-        // PHP code must use the long `<?php` tags or short-echo `<?=` tags and not other tag variations.
-        'full_opening_tag' => true,
-        // Transforms imported FQCN parameters and return types in function arguments to short version.
-        'fully_qualified_strict_types' => true,
-        // Spaces should be properly placed in a function declaration.
-        'function_declaration' => true,
-        // Ensure single space between function's argument and its typehint.
-        'function_typehint_space' => true,
-        // Convert `heredoc` to `nowdoc` where possible.
-        'heredoc_to_nowdoc' => true,
-        // Include/Require and file path should be divided with a single space. File path should not be placed under brackets.
-        'include' => true,
-        // Pre- or post-increment and decrement operators should be used if possible.
-        'increment_style' => false,
-        // Code MUST use configured indentation type.
-        'indentation_type' => true,
-        // All PHP files must use same line ending.
-        'line_ending' => true,
-        // Cast should be written in lower case.
-        'lowercase_cast' => true,
-        // PHP keywords MUST be in lower case.
-        'lowercase_keywords' => true,
-        // Class static references `self`, `static` and `parent` MUST be in lower case.
-        'lowercase_static_reference' => true,
-        // Magic constants should be referred to using the correct casing.
-        'magic_constant_casing' => true,
-        // Magic method definitions and calls must be using the correct casing.
-        'magic_method_casing' => true,
-        // In method arguments and method call, there MUST NOT be a space before each comma and there MUST be one space after each comma. Argument lists MAY be split across multiple lines, where each subsequent line is indented once. When doing so, the first item in the list MUST be on the next line, and there MUST be only one argument per line.
-        'method_argument_space' => true,
-        // Method chaining MUST be properly indented. Method chaining with different levels of indentation is not supported.
-        'method_chaining_indentation' => true,
-        // DocBlocks must start with two asterisks, multiline comments must start with a single asterisk, after the opening slash. Both must end with a single asterisk before the closing slash.
-        'multiline_comment_opening_closing' => true,
-        // Forbid multi-line whitespace before the closing semicolon or move the semicolon to the new line for chained calls.
-        'multiline_whitespace_before_semicolons' => true,
-        // Function defined by PHP should be called using the correct casing.
-        'native_function_casing' => true,
-        // Native type hints for functions should use the correct case.
-        'native_function_type_declaration_casing' => true,
-        // All instances created with new keyword must be followed by braces.
-        'new_with_braces' => true,
-        // Replace control structure alternative syntax to use braces.
-        'no_alternative_syntax' => true,
-        // There should not be a binary flag before strings.
-        'no_binary_string' => true,
-        // There should be no empty lines after class opening brace.
-        'no_blank_lines_after_class_opening' => true,
-        // There should not be blank lines between docblock and the documented element.
-        'no_blank_lines_after_phpdoc' => true,
-        // There must be a comment when fall-through is intentional in a non-empty case body.
-        'no_break_comment' => true,
-        // The closing `? >` tag MUST be omitted from files containing only PHP.
-        'no_closing_tag' => true,
-        // There should not be any empty comments.
-        'no_empty_comment' => true,
-        // There should not be empty PHPDoc blocks.
-        'no_empty_phpdoc' => true,
-        // Remove useless semicolon statements.
-        'no_empty_statement' => true,
-        // Removes extra blank lines and/or blank lines following configuration.
-        'no_extra_blank_lines' => true,
-        // Remove leading slashes in `use` clauses.
-        'no_leading_import_slash' => true,
-        // The namespace declaration line shouldn't contain leading whitespace.
-        'no_leading_namespace_whitespace' => true,
-        // Either language construct `print` or `echo` should be used.
-        'no_mixed_echo_print' => true,
-        // Operator `=>` should not be surrounded by multi-line whitespaces.
-        'no_multiline_whitespace_around_double_arrow' => true,
-        // Properties MUST not be explicitly initialized with `null` except when they have a type declaration (PHP 7.4).
-        'no_null_property_initialization' => true,
-        // Short cast `bool` using double exclamation mark should not be used.
-        'no_short_bool_cast' => true,
-        // Replace short-echo `<?=` with long format `<?php echo` syntax.
-        'no_short_echo_tag' => true,
-        // Single-line whitespace before closing semicolon are prohibited.
-        'no_singleline_whitespace_before_semicolons' => true,
-        // When making a method or function call, there MUST NOT be a space between the method or function name and the opening parenthesis.
-        'no_spaces_after_function_name' => true,
-        // There MUST NOT be spaces around offset braces.
-        'no_spaces_around_offset' => true,
-        // There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.
-        'no_spaces_inside_parenthesis' => true,
-        // Replaces superfluous `elseif` with `if`.
-        'no_superfluous_elseif' => true,
-        // Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
-        'no_superfluous_phpdoc_tags' => ['allow_mixed'=>true,'remove_inheritdoc'=>true],
-        // Remove trailing commas in list function calls.
-        'no_trailing_comma_in_list_call' => true,
-        // PHP single-line arrays should not have trailing comma.
-        'no_trailing_comma_in_singleline_array' => true,
-        // Remove trailing whitespace at the end of non-blank lines.
-        'no_trailing_whitespace' => true,
-        // There MUST be no trailing spaces inside comment or PHPDoc.
-        'no_trailing_whitespace_in_comment' => true,
-        // Removes unneeded parentheses around control statements.
-        'no_unneeded_control_parentheses' => true,
-        // Removes unneeded curly braces that are superfluous and aren't part of a control structure's body.
-        'no_unneeded_curly_braces' => ['namespaces'=>true],
-        // Variables must be set `null` instead of using `(unset)` casting.
-        'no_unset_cast' => false,
-        // Unused `use` statements must be removed.
-        'no_unused_imports' => true,
-        // There should not be useless `else` cases.
-        'no_useless_else' => true,
-        // There should not be an empty `return` statement at the end of a function.
-        'no_useless_return' => false,
-        // In array declaration, there MUST NOT be a whitespace before each comma.
-        'no_whitespace_before_comma_in_array' => true,
-        // Remove trailing whitespace at the end of blank lines.
-        'no_whitespace_in_blank_line' => true,
-        // Array index should always be written by using square braces.
-        'normalize_index_brace' => true,
-        // There should not be space before or after object `T_OBJECT_OPERATOR` `->`.
-        'object_operator_without_whitespace' => true,
-        // Orders the elements of classes/interfaces/traits.
-        'ordered_class_elements' => true,
-        // Ordering `use` statements.
-        'ordered_imports' => true,
-        // PHPUnit annotations should be a FQCNs including a root namespace.
-        'php_unit_fqcn_annotation' => false,
-        // All PHPUnit test classes should be marked as internal.
-        'php_unit_internal_class' => false,
-        // Enforce camel (or snake) case for PHPUnit test methods, following configuration.
-        'php_unit_method_casing' => false,
-        // Order `@covers` annotation of PHPUnit tests.
-        'php_unit_ordered_covers' => false,
-        // Adds a default `@coversNothing` annotation to PHPUnit test classes that have no `@covers*` annotation.
-        'php_unit_test_class_requires_covers' => false,
-        // PHPDoc should contain `@param` for all params.
-        'phpdoc_add_missing_param_annotation' => false,
-        // All items of the given phpdoc tags must be either left-aligned or (by default) aligned vertically.
-        'phpdoc_align' => true,
-        // PHPDoc annotation descriptions should not be a sentence.
-        'phpdoc_annotation_without_dot' => false,
-        // Docblocks should have the same indentation as the documented subject.
-        'phpdoc_indent' => true,
-        // `@access` annotations should be omitted from PHPDoc.
-        'phpdoc_no_access' => true,
-        // No alias PHPDoc tags should be used.
-        'phpdoc_no_alias_tag' => false,
-        // `@return void` and `@return null` annotations should be omitted from PHPDoc.
-        'phpdoc_no_empty_return' => false,
-        // `@package` and `@subpackage` annotations should be omitted from PHPDoc.
-        'phpdoc_no_package' => true,
-        // Classy that does not inherit must not have `@inheritdoc` tags.
-        'phpdoc_no_useless_inheritdoc' => true,
-        // Annotations in PHPDoc should be ordered so that `@param` annotations come first, then `@throws` annotations, then `@return` annotations.
-        'phpdoc_order' => true,
-        // The type of `@return` annotations of methods returning a reference to itself must the configured one.
-        'phpdoc_return_self_reference' => true,
-        // Scalar types should always be written in the same form. `int` not `integer`, `bool` not `boolean`, `float` not `real` or `double`.
-        'phpdoc_scalar' => true,
-        // Annotations in PHPDoc should be grouped together so that annotations of the same type immediately follow each other, and annotations of a different type are separated by a single blank line.
-        'phpdoc_separation' => true,
-        // Single line `@var` PHPDoc should have proper spacing.
-        'phpdoc_single_line_var_spacing' => true,
-        // PHPDoc summary should end in either a full stop, exclamation mark, or question mark.
-        'phpdoc_summary' => true,
-        // Docblocks should only be used on structural elements.
-        'phpdoc_to_comment' => false,
-        // PHPDoc should start and end with content, excluding the very first and last line of the docblocks.
-        'phpdoc_trim' => true,
-        // Removes extra blank lines after summary and after description in PHPDoc.
-        'phpdoc_trim_consecutive_blank_line_separation' => true,
-        // The correct case must be used for standard PHP types in PHPDoc.
-        'phpdoc_types' => true,
-        // Sorts PHPDoc types.
-        'phpdoc_types_order' => true,
-        // Convert double quotes to single quotes for simple strings.
-        'single_quote' => ['strings_containing_single_quote_chars'=>true],
-        // Visibility MUST be declared on all properties and methods; `abstract` and `final` MUST be declared before the visibility; `static` MUST be declared after the visibility.
-        'visibility_required' => ['elements'=>['property','method','const']],
-        // Use `null` coalescing operator `??` where possible. Requires PHP >= 7.0.
-        'ternary_to_null_coalescing' => true,
-        // Replaces `dirname(__FILE__)` expression with equivalent `__DIR__` constant.
-        'dir_constant' => true,
-        // Converts `static` access to `self` access in `final` classes.
-        'final_static_access' => true,
-        // Imports or fully qualifies global classes/functions/constants.
-        'global_namespace_import' => ['import_functions'=>true],
-        // Add, replace or remove header comment.
-        'header_comment' => ['comment_type'=>'comment','header'=>"Copyright MacFJA\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\nrights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the\nSoftware.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\nWARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",'location'=>'after_declare_strict'],
-        // Replaces `is_null($var)` expression with `null === $var`.
-        'is_null' => true,
-        // Ensure there is no code on the same line as the PHP open tag.
-        'linebreak_after_opening_tag' => true,
-        // Use `&&` and `||` logical operators instead of `and` and `or`.
-        'logical_operators' => true,
-        // Replaces `intval`, `floatval`, `doubleval`, `strval` and `boolval` function calls with according type casting operator.
-        'modernize_types_casting' => true,
-        // Add leading `\` before constant invocation of internal constant to speed up resolving. Constant name match is case-sensitive, except for `null`, `false` and `true`.
-        'native_constant_invocation' => true,
-        // Add leading `\` before function invocation to speed up resolving.
-        'native_function_invocation' => true,
-        // Convert PHP4-style constructors to `__construct`.
-        'no_php4_constructor' => true,
-        // Adds or removes `?` before type declarations for parameters with a default `null` value.
-        'nullable_type_declaration_for_default_null_value' => true,
-        // Changes doc blocks from single to multi line, or reversed. Works for class constants, properties and methods only.
-        'phpdoc_line_span' => ['const'=>'multi','property'=>'single'],
-        // Function `implode` must be called with 2 arguments in the documented order.
-        'implode_call' => true,
-        // Functions should be used with `$strict` param set to `true`.
-        'strict_param' => true,
-        'declare_strict_types' => true,
-    ])
-    ->setFinder(PhpCsFixer\Finder::create()
-        ->exclude('vendor')
-        ->in(__DIR__.'/src')
-        ->in(__DIR__.'/alias')
-        ->in(__DIR__.'/tests')
-    )
-    ;
\ No newline at end of file
diff --git a/.unused.dist.php b/.unused.dist.php
new file mode 100644
index 0000000..c9ff678
--- /dev/null
+++ b/.unused.dist.php
@@ -0,0 +1,36 @@
+
+<?php
+
+$projectPath = __DIR__ . '/';
+
+$scanDirectories = [
+    $projectPath . '/src/',
+    $projectPath . '/tests/',
+];
+
+return [
+    /**
+     * Required params
+     **/
+    'composerJsonPath' => $projectPath . '/composer.json',
+    'vendorPath' => $projectPath . '/vendor/',
+    'scanDirectories' => $scanDirectories,
+
+    /**
+     * Optional params
+     **/
+    'skipPackages' => [
+        'phpmd/phpmd', // QA tool
+        'vimeo/psalm', // QA tool
+        'friendsofphp/php-cs-fixer', // QA tool
+        'rskuipers/php-assumptions', // QA tool
+        'ergebnis/composer-normalize', // QA tool
+        'enlightn/security-checker', // QA tool
+        'php-parallel-lint/php-parallel-lint', // QA tool
+        'sebastian/phpcpd', // QA tool
+    ],
+    'excludeDirectories' => [],
+    'scanFiles' => [],
+    'extensions' => ['*.php'],
+    'requireDev' => true
+];
\ No newline at end of file
diff --git a/.unused.php b/.unused.php
deleted file mode 100644
index c43f02f..0000000
--- a/.unused.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-$projectPath = __DIR__ . '/';
-
-$scanDirectories = [
-    $projectPath . '/src/',
-];
-
-$scanFiles = [];
-$excludeDirectories = [];
-return [
-    /**
-     * Required params
-     **/
-    'composerJsonPath' => $projectPath . '/composer.json',
-    'vendorPath' => $projectPath . '/vendor/',
-    'scanDirectories' => $scanDirectories,
-
-    /**
-     * Optional params
-     **/
-    'skipPackages' => [
-    ],
-    'excludeDirectories' => $excludeDirectories,
-    'scanFiles' => $scanFiles,
-    'extensions' => ['*.php'],
-    'requireDev' => false
-];
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e95ff14..ace7a09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,9 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased]
+## [Unreleased v2.x]
+
+## [Unreleased v1.x]
 
 ## [1.4.0]
 
@@ -104,7 +106,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.x...HEAD
+[Unreleased v1.x]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...HEAD
 [1.4.0]: https://github.com/MacFJA/php-redisearch/compare/1.3.0...1.4.0
 [1.3.0]: https://github.com/MacFJA/php-redisearch/compare/1.2.0...1.3.0
 [1.2.0]: https://github.com/MacFJA/php-redisearch/compare/1.1.0...1.2.0
diff --git a/Makefile b/Makefile
index 01b9684..426198f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,21 +1,19 @@
-.PHONY: analyze fix-code test coverage mutation-test clean
+.PHONY: analyze fix-code test coverage mutation-test validation
 
 analyze: | vendor
 	$(COMPOSER) exec -v parallel-lint -- src
 	$(COMPOSER) exec -v php-cs-fixer -- fix --dry-run
-	$(COMPOSER) exec -v unused_scanner -- .unused.php
+	$(COMPOSER) exec -v unused_scanner -- .unused.dist.php
 	$(COMPOSER) exec -v security-checker -- security:check
 	$(COMPOSER) exec -v phpcpd -- --fuzzy src
-	$(COMPOSER) exec -v phpmd -- src ansi phpmd.xml
+	$(COMPOSER) exec -v phpmd -- src ansi codesize,controversial,design,naming,unusedcode
 	$(COMPOSER) exec -v phpa -- src
-	$(COMPOSER) exec -v phpstan -- analyse --level=8 src
-	$(COMPOSER) exec -v psalm -- --show-info=true src
-	$(COMPOSER) exec -v phan -- --allow-polyfill-parser --color --color-scheme=light --output-mode=text
+	$(COMPOSER) exec -v phpstan -- analyse
+	$(COMPOSER) exec -v psalm -- src
 
-fix-code: |vendor
+fix-code: | vendor
 	$(COMPOSER) normalize
 	$(COMPOSER) exec -v php-cs-fixer -- fix
-	@#$(COMPOSER) exec -v psalm -- --alter --issues=all src
 
 test: | vendor
 	$(COMPOSER) exec -v phpunit
@@ -24,13 +22,7 @@ coverage: | vendor
 	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
 	$(COMPOSER) exec -v phpunit -- --coverage-text --color
 
-mutation-test: | vendor
-	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
-	$(COMPOSER) exec -v infection -- --only-covered --min-covered-msi=95
-
-clean:
-	rm -rf .phpunit.cache vendor
-	rm -f .php_cs.cache .phpunit.result.cache composer.phar composer.lock infection.log
+validation: fix-code analyze test coverage
 
 vendor: composer.json
 	$(COMPOSER) install --optimize-autoloader --no-suggest --prefer-dist
@@ -49,4 +41,4 @@ ifneq ($(shell command -v composer > /dev/null ; echo $$?), 0)
   COMPOSER=php composer.phar
 else
   COMPOSER=composer
-endif
+endif
\ No newline at end of file
diff --git a/README.md b/README.md
index 4a907ce..4a8907b 100644
--- a/README.md
+++ b/README.md
@@ -16,21 +16,25 @@ composer require macfja/redisearch
 
 ```php
 $client = new \Predis\Client(/* ... */);
-$builder = new \MacFJA\RediSearch\Index\Builder($client);
+$builder = new \MacFJA\RediSearch\IndexBuilder();
 
 // Field can be create in advance
-$address = new \MacFJA\RediSearch\Index\Builder\GeoField('address');
+$address = (new \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption())
+    ->setField('address');
 
 $builder
-    ->withName('person')
+    ->setIndex('person')
     ->addField($address)
     // Or field can be create "inline"
     ->addTextField('lastname', false, null, null, true)
     ->addTextField('firstname')
     ->addNumericField('age')
-    ->create();
+    ->create($client);
 ```
 
+The builder can also be used with `withXxx` and `withAddedXxx` instead of `setXxx` and `addXxx`.
+This will give you a new instance of the builder with the configured data.
+
 ### Add a document
 
 ```php
@@ -48,82 +52,84 @@ $index->addFromArray([
 
 ```php
 $client = new \Predis\Client(/* ... */);
-$search = new \MacFJA\RediSearch\Search($client);
-
-$results = $search
-    ->withIndex('person')
-    ->withQuery('Doe')
-    ->withHighlight(['lastname'])
-    ->withScores()
-    ->search();
+$search = new \MacFJA\RediSearch\Redis\Command\Search();
+
+$search
+    ->setIndex('person')
+    ->setQuery('Doe')
+    ->setHighlight(['lastname'])
+    ->setWithScores();
+$results = $client->executeCommand($search);
 ```
 
 #### Create a search query
 
 ```php
-use \MacFJA\RediSearch\Search\QueryBuilder\NumericFacet;
-use \MacFJA\RediSearch\Search\QueryBuilder\Negation;
-use \MacFJA\RediSearch\Search\QueryBuilder\GeoFacet;
-use \MacFJA\RediSearch\Search\QueryBuilder\Optional;
-use \MacFJA\RediSearch\Search\QueryBuilder\Word;
-use \MacFJA\RediSearch\Search\GeoFilter;
-
-$queryBuilder = \MacFJA\RediSearch\Search\QueryBuilder::create();
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use MacFJA\RediSearch\Query\Builder\Negation;
+use MacFJA\RediSearch\Query\Builder\NumericFacet;
+use MacFJA\RediSearch\Query\Builder\Optional;
+use MacFJA\RediSearch\Query\Builder\Word;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+
+$queryBuilder = new \MacFJA\RediSearch\Query\Builder();
 $query = $queryBuilder
-    ->addExpression(NumericFacet::greaterThan('age', 17))
+    ->addElement(NumericFacet::greaterThan('age', 17))
     ->addString('Doe')
-    ->addExpression(
+    ->addElement(
         new Negation(
-            new GeoFacet('address', 40.589247, -74.044502, 40, GeoFilter::UNIT_KILOMETERS)
+            new GeoFacet(['address'], 40.589247, -74.044502, 40, GeoFilterOption::UNIT_KILOMETERS)
         )
     )
-    ->addExpression(new Optional(new Word('John')))
+    ->addElement(new Optional(new Word('John')))
     ->render();
 
 // The value of $query is:
-// Doe ~John @age:[(17 +inf] -@address:[40.589247 -74.044502 40.000000 km]
+// @age:[(17 +inf] Doe -@address:[40.589247 -74.044502 40.000000 km] ~John
 ```
 
 ### Pipeline
 
 ```php
-use MacFJA\RediSearch\Aggregate;
-use MacFJA\RediSearch\Aggregate\Reducer;
-use MacFJA\RediSearch\Pipeline;
-use MacFJA\RediSearch\Search;
-use MacFJA\RediSearch\Suggestions;
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
+use MacFJA\RediSearch\Redis\Command\Search;
+use MacFJA\RediSearch\Redis\Command\SugGet;
 use Predis\Client;
 
 $client = new Client(/* ... */);
-$suggestion = new Suggestions($client);
-$pipeline = new Pipeline($client);
-$query = '@age:[(17 +inf] %john%';
-$search = new Search($client);
-$aggregate = new Aggregate($client);
 
-$pipeline
-    ->addPipeable(
-        $search->withIndex('people')->withQuery($query)
-    )
-    ->addPipeable(
-        $aggregate
-            ->withIndexName('people')
-            ->withQuery($query)
-            ->addGroupBy([], [
-                Reducer::average('age', 'avg'),
-                Reducer::maximum('age', 'oldest')
-            ])
-    )
-    ->addPipeable(
-        $aggregate
-            ->withIndexName('people')
-            ->withQuery($query)
-            ->addGroupBy(['lastname'], [Reducer::count('count')])
-    )
-    ->addItem(
-        $suggestion->pipeableGet('john', true)
-    );
-$result = $pipeline->executePipeline();
+$query = '@age:[(17 +inf] %john%';
+$search = new Search();
+$search->setIndex('people')
+    ->setQuery($query);
+
+$stats = new Aggregate();
+$stats->setIndex('people')
+    ->setQuery($query)
+    ->addGroupBy(new GroupByOption([], [
+        ReduceOption::average('age', 'avg'),
+        ReduceOption::maximum('age', 'oldest')
+    ]));
+
+$aggregate = new Aggregate();
+$aggregate->setIndex('people')
+    ->setQuery($query)
+    ->addGroupBy(new GroupByOption(['lastname'], [ReduceOption::count('count')]));
+
+$suggestion = new SugGet();
+$suggestion->setDictionary('names')
+    ->setPrefix('john')
+    ->setFuzzy();
+
+$result = $client->pipeline()
+    ->executeCommand($search)
+    ->executeCommand($stats)
+    ->executeCommand($aggregate)
+    ->executeCommand($suggestion)
+    ->execute()
+;
 
 // $result[0] is the search result
 // $result[1] is the first aggregation result
@@ -131,6 +137,16 @@ $result = $pipeline->executePipeline();
 // $result[3] is the suggestion result
 ```
 
+### Use Predis shorthand syntax
+
+```php
+$client = new \Predis\Client(/* ... */);
+\MacFJA\RediSearch\Redis\Initializer::registerCommands($client->getProfile());
+
+$client->ftsearch('people', '@age:[(17 +inf] %john%');
+// But you will have raw Redis output.
+```
+
 ## Similar projects
 
 - [ethanhann/redisearch-php](https://packagist.org/packages/ethanhann/redisearch-php) - Abandoned
@@ -148,7 +164,7 @@ To do so, you have Github issues to:
 You also have PR to:
  - suggest a correction
  - suggest a new feature
- - and much more... 
+ - and much more...
  
 See [CONTRIBUTING](CONTRIBUTING.md) for more information.
 
diff --git a/alias/Aggregate.php b/alias/Aggregate.php
deleted file mode 100644
index e9d56a6..0000000
--- a/alias/Aggregate.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate */
-final class Aggregate extends \MacFJA\RediSearch\Aggregate
-{
-}
diff --git a/alias/Aggregate/Exception/NotEnoughPropertiesException.php b/alias/Aggregate/Exception/NotEnoughPropertiesException.php
deleted file mode 100644
index d0f22ad..0000000
--- a/alias/Aggregate/Exception/NotEnoughPropertiesException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Aggregate\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\Exception\NotEnoughPropertiesException */
-final class NotEnoughPropertiesException extends \MacFJA\RediSearch\Aggregate\Exception\NotEnoughPropertiesException
-{
-}
diff --git a/alias/Aggregate/Exception/NotEnoughReducersException.php b/alias/Aggregate/Exception/NotEnoughReducersException.php
deleted file mode 100644
index 56153fd..0000000
--- a/alias/Aggregate/Exception/NotEnoughReducersException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Aggregate\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\Exception\NotEnoughReducersException */
-final class NotEnoughReducersException extends \MacFJA\RediSearch\Aggregate\Exception\NotEnoughReducersException
-{
-}
diff --git a/alias/Aggregate/Exception/UnknownSortDirectionException.php b/alias/Aggregate/Exception/UnknownSortDirectionException.php
deleted file mode 100644
index b68f154..0000000
--- a/alias/Aggregate/Exception/UnknownSortDirectionException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Aggregate\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\Exception\UnknownSortDirectionException */
-final class UnknownSortDirectionException extends \MacFJA\RediSearch\Aggregate\Exception\UnknownSortDirectionException
-{
-}
diff --git a/alias/Aggregate/Result.php b/alias/Aggregate/Result.php
deleted file mode 100644
index ca12774..0000000
--- a/alias/Aggregate/Result.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Aggregate;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\Result */
-final class Result extends \MacFJA\RediSearch\Aggregate\Result
-{
-}
diff --git a/alias/Aggregate/SortBy.php b/alias/Aggregate/SortBy.php
deleted file mode 100644
index efa4e1c..0000000
--- a/alias/Aggregate/SortBy.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Aggregate;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\SortBy */
-final class SortBy extends \MacFJA\RediSearch\Aggregate\SortBy
-{
-}
diff --git a/alias/Builder.php b/alias/Builder.php
deleted file mode 100644
index 9e28424..0000000
--- a/alias/Builder.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Builder */
-interface Builder extends \MacFJA\RediSearch\Builder
-{
-}
diff --git a/alias/Configuration.php b/alias/Configuration.php
deleted file mode 100644
index 4fcba6b..0000000
--- a/alias/Configuration.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Configuration */
-final class Configuration extends \MacFJA\RediSearch\Configuration
-{
-}
diff --git a/alias/Helper/DataHelper.php b/alias/Helper/DataHelper.php
deleted file mode 100644
index 6d3c2c3..0000000
--- a/alias/Helper/DataHelper.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Helper;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Helper\DataHelper */
-final class DataHelper extends \MacFJA\RediSearch\Helper\DataHelper
-{
-}
diff --git a/alias/Helper/RedisHelper.php b/alias/Helper/RedisHelper.php
deleted file mode 100644
index 0838239..0000000
--- a/alias/Helper/RedisHelper.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Helper;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Helper\RedisHelper */
-final class RedisHelper extends \MacFJA\RediSearch\Helper\RedisHelper
-{
-}
diff --git a/alias/Index/Builder.php b/alias/Index/Builder.php
deleted file mode 100644
index f8446e2..0000000
--- a/alias/Index/Builder.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder */
-final class Builder extends \MacFJA\RediSearch\Index\Builder
-{
-}
diff --git a/alias/Index/Builder/AbstractField.php b/alias/Index/Builder/AbstractField.php
deleted file mode 100644
index e3fce7e..0000000
--- a/alias/Index/Builder/AbstractField.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\AbstractField */
-final class AbstractField extends \MacFJA\RediSearch\Index\Builder\AbstractField
-{
-}
diff --git a/alias/Index/Builder/Exception/MissingNameException.php b/alias/Index/Builder/Exception/MissingNameException.php
deleted file mode 100644
index 1bed956..0000000
--- a/alias/Index/Builder/Exception/MissingNameException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\Exception\MissingNameException */
-final class MissingNameException extends \MacFJA\RediSearch\Index\Builder\Exception\MissingNameException
-{
-}
diff --git a/alias/Index/Builder/Exception/MustBeSingleCharException.php b/alias/Index/Builder/Exception/MustBeSingleCharException.php
deleted file mode 100644
index c5ca43f..0000000
--- a/alias/Index/Builder/Exception/MustBeSingleCharException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\Exception\MustBeSingleCharException */
-final class MustBeSingleCharException extends \MacFJA\RediSearch\Index\Builder\Exception\MustBeSingleCharException
-{
-}
diff --git a/alias/Index/Builder/Exception/TooManyFields.php b/alias/Index/Builder/Exception/TooManyFields.php
deleted file mode 100644
index 975dbc8..0000000
--- a/alias/Index/Builder/Exception/TooManyFields.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\Exception\TooManyFields */
-final class TooManyFields extends \MacFJA\RediSearch\Index\Builder\Exception\TooManyFields
-{
-}
diff --git a/alias/Index/Builder/Exception/UnsupportedLanguageException.php b/alias/Index/Builder/Exception/UnsupportedLanguageException.php
deleted file mode 100644
index 1fd1a0a..0000000
--- a/alias/Index/Builder/Exception/UnsupportedLanguageException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\Exception\UnsupportedLanguageException */
-final class UnsupportedLanguageException extends \MacFJA\RediSearch\Index\Builder\Exception\UnsupportedLanguageException
-{
-}
diff --git a/alias/Index/Builder/Field.php b/alias/Index/Builder/Field.php
deleted file mode 100644
index 5a4e160..0000000
--- a/alias/Index/Builder/Field.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\Field */
-interface Field extends \MacFJA\RediSearch\Index\Builder\Field
-{
-}
diff --git a/alias/Index/Builder/GeoField.php b/alias/Index/Builder/GeoField.php
deleted file mode 100644
index 78bf59f..0000000
--- a/alias/Index/Builder/GeoField.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\GeoField */
-final class GeoField extends \MacFJA\RediSearch\Index\Builder\GeoField
-{
-}
diff --git a/alias/Index/Builder/NumericField.php b/alias/Index/Builder/NumericField.php
deleted file mode 100644
index c4b29c5..0000000
--- a/alias/Index/Builder/NumericField.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\NumericField */
-final class NumericField extends \MacFJA\RediSearch\Index\Builder\NumericField
-{
-}
diff --git a/alias/Index/Builder/TagField.php b/alias/Index/Builder/TagField.php
deleted file mode 100644
index f3f3e18..0000000
--- a/alias/Index/Builder/TagField.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\TagField */
-final class TagField extends \MacFJA\RediSearch\Index\Builder\TagField
-{
-}
diff --git a/alias/Index/Builder/TextField.php b/alias/Index/Builder/TextField.php
deleted file mode 100644
index 7c40a0d..0000000
--- a/alias/Index/Builder/TextField.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Builder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Builder\TextField */
-final class TextField extends \MacFJA\RediSearch\Index\Builder\TextField
-{
-}
diff --git a/alias/Index/Exception/IndexNotFoundException.php b/alias/Index/Exception/IndexNotFoundException.php
deleted file mode 100644
index d8e018b..0000000
--- a/alias/Index/Exception/IndexNotFoundException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\Exception\IndexNotFoundException */
-final class IndexNotFoundException extends \MacFJA\RediSearch\Index\Exception\IndexNotFoundException
-{
-}
diff --git a/alias/Index/InfoResult.php b/alias/Index/InfoResult.php
deleted file mode 100644
index ffa248a..0000000
--- a/alias/Index/InfoResult.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Index;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index\InfoResult */
-final class InfoResult extends \MacFJA\RediSearch\Index\InfoResult
-{
-}
diff --git a/alias/PartialQuery.php b/alias/PartialQuery.php
deleted file mode 100644
index 13ba0a9..0000000
--- a/alias/PartialQuery.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\PartialQuery */
-interface PartialQuery extends \MacFJA\RediSearch\PartialQuery
-{
-}
diff --git a/alias/Search/Exception/UnknownUnitException.php b/alias/Search/Exception/UnknownUnitException.php
deleted file mode 100644
index 2300a68..0000000
--- a/alias/Search/Exception/UnknownUnitException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\Exception\UnknownUnitException */
-final class UnknownUnitException extends \MacFJA\RediSearch\Search\Exception\UnknownUnitException
-{
-}
diff --git a/alias/Search/Exception/UnsupportedLanguageException.php b/alias/Search/Exception/UnsupportedLanguageException.php
deleted file mode 100644
index 43c16e3..0000000
--- a/alias/Search/Exception/UnsupportedLanguageException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\Exception\UnsupportedLanguageException */
-final class UnsupportedLanguageException extends \MacFJA\RediSearch\Search\Exception\UnsupportedLanguageException
-{
-}
diff --git a/alias/Search/Filter.php b/alias/Search/Filter.php
deleted file mode 100644
index 7ee524e..0000000
--- a/alias/Search/Filter.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\Filter */
-final class Filter extends \MacFJA\RediSearch\Search\Filter
-{
-}
diff --git a/alias/Search/GeoFilter.php b/alias/Search/GeoFilter.php
deleted file mode 100644
index 08e3185..0000000
--- a/alias/Search/GeoFilter.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\GeoFilter */
-final class GeoFilter extends \MacFJA\RediSearch\Search\GeoFilter
-{
-}
diff --git a/alias/Search/Highlight.php b/alias/Search/Highlight.php
deleted file mode 100644
index 3ac4b15..0000000
--- a/alias/Search/Highlight.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\Highlight */
-final class Highlight extends \MacFJA\RediSearch\Search\Highlight
-{
-}
diff --git a/alias/Search/QueryBuilder.php b/alias/Search/QueryBuilder.php
deleted file mode 100644
index 7b651dc..0000000
--- a/alias/Search/QueryBuilder.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder */
-final class QueryBuilder extends \MacFJA\RediSearch\Search\QueryBuilder
-{
-}
diff --git a/alias/Search/QueryBuilder/AndGroup.php b/alias/Search/QueryBuilder/AndGroup.php
deleted file mode 100644
index 12b2395..0000000
--- a/alias/Search/QueryBuilder/AndGroup.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\AndGroup */
-final class AndGroup extends \MacFJA\RediSearch\Search\QueryBuilder\AndGroup
-{
-}
diff --git a/alias/Search/QueryBuilder/ExactMatch.php b/alias/Search/QueryBuilder/ExactMatch.php
deleted file mode 100644
index 7c05c2b..0000000
--- a/alias/Search/QueryBuilder/ExactMatch.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\ExactMatch */
-final class ExactMatch extends \MacFJA\RediSearch\Search\QueryBuilder\ExactMatch
-{
-}
diff --git a/alias/Search/QueryBuilder/FuzzyWord.php b/alias/Search/QueryBuilder/FuzzyWord.php
deleted file mode 100644
index b731ede..0000000
--- a/alias/Search/QueryBuilder/FuzzyWord.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\FuzzyWord */
-final class FuzzyWord extends \MacFJA\RediSearch\Search\QueryBuilder\FuzzyWord
-{
-}
diff --git a/alias/Search/QueryBuilder/GeoFacet.php b/alias/Search/QueryBuilder/GeoFacet.php
deleted file mode 100644
index 7da79f5..0000000
--- a/alias/Search/QueryBuilder/GeoFacet.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\GeoFacet */
-final class GeoFacet extends \MacFJA\RediSearch\Search\QueryBuilder\GeoFacet
-{
-}
diff --git a/alias/Search/QueryBuilder/GroupPartialQuery.php b/alias/Search/QueryBuilder/GroupPartialQuery.php
deleted file mode 100644
index 12ebb4f..0000000
--- a/alias/Search/QueryBuilder/GroupPartialQuery.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\GroupPartialQuery */
-interface GroupPartialQuery extends \MacFJA\RediSearch\Search\QueryBuilder\GroupPartialQuery
-{
-}
diff --git a/alias/Search/QueryBuilder/Negation.php b/alias/Search/QueryBuilder/Negation.php
deleted file mode 100644
index 451a1ac..0000000
--- a/alias/Search/QueryBuilder/Negation.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\Negation */
-final class Negation extends \MacFJA\RediSearch\Search\QueryBuilder\Negation
-{
-}
diff --git a/alias/Search/QueryBuilder/NumericFacet.php b/alias/Search/QueryBuilder/NumericFacet.php
deleted file mode 100644
index 95458f8..0000000
--- a/alias/Search/QueryBuilder/NumericFacet.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\NumericFacet */
-final class NumericFacet extends \MacFJA\RediSearch\Search\QueryBuilder\NumericFacet
-{
-}
diff --git a/alias/Search/QueryBuilder/Optional.php b/alias/Search/QueryBuilder/Optional.php
deleted file mode 100644
index c454a38..0000000
--- a/alias/Search/QueryBuilder/Optional.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\Optional */
-final class Optional extends \MacFJA\RediSearch\Search\QueryBuilder\Optional
-{
-}
diff --git a/alias/Search/QueryBuilder/OrGroup.php b/alias/Search/QueryBuilder/OrGroup.php
deleted file mode 100644
index 802d6bd..0000000
--- a/alias/Search/QueryBuilder/OrGroup.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\OrGroup */
-final class OrGroup extends \MacFJA\RediSearch\Search\QueryBuilder\OrGroup
-{
-}
diff --git a/alias/Search/QueryBuilder/PartialQuery.php b/alias/Search/QueryBuilder/PartialQuery.php
deleted file mode 100644
index dad1dcc..0000000
--- a/alias/Search/QueryBuilder/PartialQuery.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\PartialQuery */
-interface PartialQuery extends \MacFJA\RediSearch\Search\QueryBuilder\PartialQuery
-{
-}
diff --git a/alias/Search/QueryBuilder/QueryExpressionAttribute.php b/alias/Search/QueryBuilder/QueryExpressionAttribute.php
deleted file mode 100644
index 0b76597..0000000
--- a/alias/Search/QueryBuilder/QueryExpressionAttribute.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\QueryExpressionAttribute */
-final class QueryExpressionAttribute extends \MacFJA\RediSearch\Search\QueryBuilder\QueryExpressionAttribute
-{
-}
diff --git a/alias/Search/QueryBuilder/TagFacet.php b/alias/Search/QueryBuilder/TagFacet.php
deleted file mode 100644
index 77d0daa..0000000
--- a/alias/Search/QueryBuilder/TagFacet.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\TagFacet */
-final class TagFacet extends \MacFJA\RediSearch\Search\QueryBuilder\TagFacet
-{
-}
diff --git a/alias/Search/QueryBuilder/TextFacet.php b/alias/Search/QueryBuilder/TextFacet.php
deleted file mode 100644
index 948def1..0000000
--- a/alias/Search/QueryBuilder/TextFacet.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\TextFacet */
-final class TextFacet extends \MacFJA\RediSearch\Search\QueryBuilder\TextFacet
-{
-}
diff --git a/alias/Search/QueryBuilder/Word.php b/alias/Search/QueryBuilder/Word.php
deleted file mode 100644
index f000840..0000000
--- a/alias/Search/QueryBuilder/Word.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search\QueryBuilder;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\QueryBuilder\Word */
-final class Word extends \MacFJA\RediSearch\Search\QueryBuilder\Word
-{
-}
diff --git a/alias/Search/Result.php b/alias/Search/Result.php
deleted file mode 100644
index 550feae..0000000
--- a/alias/Search/Result.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\Result */
-final class Result extends \MacFJA\RediSearch\Search\Result
-{
-}
diff --git a/alias/Search/Summarize.php b/alias/Search/Summarize.php
deleted file mode 100644
index c27c7e4..0000000
--- a/alias/Search/Summarize.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Search;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search\Summarize */
-final class Summarize extends \MacFJA\RediSearch\Search\Summarize
-{
-}
diff --git a/alias/Suggestion/Result.php b/alias/Suggestion/Result.php
deleted file mode 100644
index 43ff7a7..0000000
--- a/alias/Suggestion/Result.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Suggestion;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Suggestion\Result */
-final class Result extends \MacFJA\RediSearch\Suggestion\Result
-{
-}
diff --git a/alias/Suggestions.php b/alias/Suggestions.php
deleted file mode 100644
index 70251dd..0000000
--- a/alias/Suggestions.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Suggestions */
-final class Suggestions extends \MacFJA\RediSearch\Suggestions
-{
-}
diff --git a/alias/Words.php b/alias/Words.php
deleted file mode 100644
index 38ebbe1..0000000
--- a/alias/Words.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Words */
-final class Words extends \MacFJA\RediSearch\Words
-{
-}
diff --git a/alias/Words/Exception/NotEnoughTermsException.php b/alias/Words/Exception/NotEnoughTermsException.php
deleted file mode 100644
index c3425ac..0000000
--- a/alias/Words/Exception/NotEnoughTermsException.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Words\Exception;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Words\Exception\NotEnoughTermsException */
-final class NotEnoughTermsException extends \MacFJA\RediSearch\Words\Exception\NotEnoughTermsException
-{
-}
diff --git a/alias/Words/SynonymResult.php b/alias/Words/SynonymResult.php
deleted file mode 100644
index 8eec560..0000000
--- a/alias/Words/SynonymResult.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RedisSearch\Words;
-
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Words\SynonymResult */
-final class SynonymResult extends \MacFJA\RediSearch\Words\SynonymResult
-{
-}
diff --git a/composer.json b/composer.json
index ec229b8..bc15b70 100644
--- a/composer.json
+++ b/composer.json
@@ -16,34 +16,38 @@
         }
     ],
     "require": {
-        "php": ">= 7.1",
+        "php": "^7.2",
+        "composer/semver": "^3.2",
         "predis/predis": "^1.1",
-        "sgh/comparable": "^1.1"
+        "respect/validation": "^2.0"
     },
     "require-dev": {
-        "enlightn/security-checker": "^1.5",
+        "enlightn/security-checker": "^1.9",
         "ergebnis/composer-normalize": "^2.13",
-        "friendsofphp/php-cs-fixer": "^2.16",
-        "infection/infection": "^0.18.2",
-        "insolita/unused-scanner": "^2.2",
-        "phan/phan": "^3.2",
-        "php-parallel-lint/php-parallel-lint": "^1.2",
-        "phpmd/phpmd": "^2.9",
-        "phpstan/phpstan": "^0.12.57",
-        "phpunit/phpunit": "^9.5",
+        "friendsofphp/php-cs-fixer": "^3.0",
+        "insolita/unused-scanner": "^2.3",
+        "php-parallel-lint/php-parallel-lint": "^1.3",
+        "phpmd/phpmd": "^2.10",
+        "phpstan/phpstan": "^0.12.92",
+        "phpunit/phpunit": "^8.5",
+        "roave/security-advisories": "dev-latest",
         "rskuipers/php-assumptions": "^0.8.0",
-        "sebastian/phpcpd": "^6.0",
-        "vimeo/psalm": "^4.2"
+        "sebastian/phpcpd": "^4.1",
+        "vimeo/psalm": "^4.7"
+    },
+    "config": {
+        "platform": {
+            "php": "7.2"
+        }
     },
     "autoload": {
         "psr-4": {
-            "MacFJA\\RediSearch\\": "src/",
-            "MacFJA\\RedisSearch\\": "alias/"
+            "MacFJA\\RediSearch\\": "src/"
         }
     },
     "autoload-dev": {
         "psr-4": {
-            "Tests\\MacFJA\\RediSearch\\": "tests/"
+            "MacFJA\\RediSearch\\tests\\": "tests/"
         }
     },
     "support": {
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..91087af
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,6439 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "b00accba6f256ea837d7e330fa104da9",
+    "packages": [
+        {
+            "name": "composer/semver",
+            "version": "3.2.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/semver.git",
+                "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9",
+                "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.54",
+                "symfony/phpunit-bridge": "^4.2 || ^5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\Semver\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nils Adermann",
+                    "email": "naderman@naderman.de",
+                    "homepage": "http://www.naderman.de"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                },
+                {
+                    "name": "Rob Bast",
+                    "email": "rob.bast@gmail.com",
+                    "homepage": "http://robbast.nl"
+                }
+            ],
+            "description": "Semver library that offers utilities, version constraint parsing and validation.",
+            "keywords": [
+                "semantic",
+                "semver",
+                "validation",
+                "versioning"
+            ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/semver/issues",
+                "source": "https://github.com/composer/semver/tree/3.2.5"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-24T12:41:47+00:00"
+        },
+        {
+            "name": "predis/predis",
+            "version": "v1.1.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/predis/predis.git",
+                "reference": "b240daa106d4e02f0c5b7079b41e31ddf66fddf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/predis/predis/zipball/b240daa106d4e02f0c5b7079b41e31ddf66fddf8",
+                "reference": "b240daa106d4e02f0c5b7079b41e31ddf66fddf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.8"
+            },
+            "suggest": {
+                "ext-curl": "Allows access to Webdis when paired with phpiredis",
+                "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Predis\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniele Alessandri",
+                    "email": "suppakilla@gmail.com",
+                    "homepage": "http://clorophilla.net",
+                    "role": "Creator & Maintainer"
+                },
+                {
+                    "name": "Till Krüss",
+                    "homepage": "https://till.im",
+                    "role": "Maintainer"
+                }
+            ],
+            "description": "Flexible and feature-complete Redis client for PHP and HHVM",
+            "homepage": "http://github.com/predis/predis",
+            "keywords": [
+                "nosql",
+                "predis",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/predis/predis/issues",
+                "source": "https://github.com/predis/predis/tree/v1.1.7"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sponsors/tillkruss",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-04-04T19:34:46+00:00"
+        },
+        {
+            "name": "respect/stringifier",
+            "version": "0.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Respect/Stringifier.git",
+                "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
+                "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.8",
+                "malukenho/docheader": "^0.1.7",
+                "phpunit/phpunit": "^6.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Respect\\Stringifier\\": "src/"
+                },
+                "files": [
+                    "src/stringify.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Respect/Stringifier Contributors",
+                    "homepage": "https://github.com/Respect/Stringifier/graphs/contributors"
+                }
+            ],
+            "description": "Converts any value to a string",
+            "homepage": "http://respect.github.io/Stringifier/",
+            "keywords": [
+                "respect",
+                "stringifier",
+                "stringify"
+            ],
+            "support": {
+                "issues": "https://github.com/Respect/Stringifier/issues",
+                "source": "https://github.com/Respect/Stringifier/tree/0.2.0"
+            },
+            "time": "2017-12-29T19:39:25+00:00"
+        },
+        {
+            "name": "respect/validation",
+            "version": "2.0.17",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Respect/Validation.git",
+                "reference": "314aa36e37dd3812b7d80c7bda3e83da5ee22d11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Respect/Validation/zipball/314aa36e37dd3812b7d80c7bda3e83da5ee22d11",
+                "reference": "314aa36e37dd3812b7d80c7bda3e83da5ee22d11",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2",
+                "respect/stringifier": "^0.2.0",
+                "symfony/polyfill-mbstring": "^1.2"
+            },
+            "require-dev": {
+                "egulias/email-validator": "^2.1",
+                "malukenho/docheader": "^0.1",
+                "mikey179/vfsstream": "^1.6",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpunit/phpunit": "^7.5",
+                "respect/coding-standard": "^2.1",
+                "squizlabs/php_codesniffer": "^3.5",
+                "symfony/validator": "^3.0||^4.0",
+                "zendframework/zend-validator": "^2.1"
+            },
+            "suggest": {
+                "egulias/email-validator": "Strict (RFC compliant) email validation",
+                "ext-bcmath": "Arbitrary Precision Mathematics",
+                "ext-fileinfo": "File Information",
+                "ext-mbstring": "Multibyte String Functions",
+                "symfony/validator": "Use Symfony validator through Respect\\Validation",
+                "zendframework/zend-validator": "Use Zend Framework validator through Respect\\Validation"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Respect\\Validation\\": "library/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Respect/Validation Contributors",
+                    "homepage": "https://github.com/Respect/Validation/graphs/contributors"
+                }
+            ],
+            "description": "The most awesome validation engine ever created for PHP",
+            "homepage": "http://respect.github.io/Validation/",
+            "keywords": [
+                "respect",
+                "validation",
+                "validator"
+            ],
+            "support": {
+                "issues": "https://github.com/Respect/Validation/issues",
+                "source": "https://github.com/Respect/Validation/tree/workflows/update-tld"
+            },
+            "time": "2020-08-23T00:24:43+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+                "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-27T09:27:20+00:00"
+        }
+    ],
+    "packages-dev": [
+        {
+            "name": "amphp/amp",
+            "version": "v2.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/amp.git",
+                "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9",
+                "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1",
+                "ext-json": "*",
+                "jetbrains/phpstorm-stubs": "^2019.3",
+                "phpunit/phpunit": "^6.0.9 | ^7",
+                "psalm/phar": "^3.11@dev",
+                "react/promise": "^2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Amp\\": "lib"
+                },
+                "files": [
+                    "lib/functions.php",
+                    "lib/Internal/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniel Lowrey",
+                    "email": "rdlowrey@php.net"
+                },
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                },
+                {
+                    "name": "Bob Weinand",
+                    "email": "bobwei9@hotmail.com"
+                },
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "A non-blocking concurrency framework for PHP applications.",
+            "homepage": "http://amphp.org/amp",
+            "keywords": [
+                "async",
+                "asynchronous",
+                "awaitable",
+                "concurrency",
+                "event",
+                "event-loop",
+                "future",
+                "non-blocking",
+                "promise"
+            ],
+            "support": {
+                "irc": "irc://irc.freenode.org/amphp",
+                "issues": "https://github.com/amphp/amp/issues",
+                "source": "https://github.com/amphp/amp/tree/v2.5.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-01-10T17:06:37+00:00"
+        },
+        {
+            "name": "amphp/byte-stream",
+            "version": "v1.8.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/byte-stream.git",
+                "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
+                "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1.4",
+                "friendsofphp/php-cs-fixer": "^2.3",
+                "jetbrains/phpstorm-stubs": "^2019.3",
+                "phpunit/phpunit": "^6 || ^7 || ^8",
+                "psalm/phar": "^3.11.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Amp\\ByteStream\\": "lib"
+                },
+                "files": [
+                    "lib/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                },
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "A stream abstraction to make working with non-blocking I/O simple.",
+            "homepage": "http://amphp.org/byte-stream",
+            "keywords": [
+                "amp",
+                "amphp",
+                "async",
+                "io",
+                "non-blocking",
+                "stream"
+            ],
+            "support": {
+                "irc": "irc://irc.freenode.org/amphp",
+                "issues": "https://github.com/amphp/byte-stream/issues",
+                "source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-03-30T17:13:30+00:00"
+        },
+        {
+            "name": "composer/package-versions-deprecated",
+            "version": "1.11.99.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/package-versions-deprecated.git",
+                "reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c6522afe5540d5fc46675043d3ed5a45a740b27c",
+                "reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.1.0 || ^2.0",
+                "php": "^7 || ^8"
+            },
+            "replace": {
+                "ocramius/package-versions": "1.11.99"
+            },
+            "require-dev": {
+                "composer/composer": "^1.9.3 || ^2.0@dev",
+                "ext-zip": "^1.13",
+                "phpunit/phpunit": "^6.5 || ^7"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "PackageVersions\\Installer",
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PackageVersions\\": "src/PackageVersions"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be"
+                }
+            ],
+            "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
+            "support": {
+                "issues": "https://github.com/composer/package-versions-deprecated/issues",
+                "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.2"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-24T07:46:03+00:00"
+        },
+        {
+            "name": "composer/xdebug-handler",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/xdebug-handler.git",
+                "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496",
+                "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0 || ^8.0",
+                "psr/log": "^1.0"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.55",
+                "symfony/phpunit-bridge": "^4.2 || ^5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Composer\\XdebugHandler\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "John Stevenson",
+                    "email": "john-stevenson@blueyonder.co.uk"
+                }
+            ],
+            "description": "Restarts a process without Xdebug.",
+            "keywords": [
+                "Xdebug",
+                "performance"
+            ],
+            "support": {
+                "irc": "irc://irc.freenode.org/composer",
+                "issues": "https://github.com/composer/xdebug-handler/issues",
+                "source": "https://github.com/composer/xdebug-handler/tree/2.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-05T19:37:51+00:00"
+        },
+        {
+            "name": "dnoegel/php-xdg-base-dir",
+            "version": "v0.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dnoegel/php-xdg-base-dir.git",
+                "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
+                "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "XdgBaseDir\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "implementation of xdg base directory specification for php",
+            "support": {
+                "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues",
+                "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1"
+            },
+            "time": "2019-12-04T15:06:13+00:00"
+        },
+        {
+            "name": "doctrine/annotations",
+            "version": "1.13.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/annotations.git",
+                "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f",
+                "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/lexer": "1.*",
+                "ext-tokenizer": "*",
+                "php": "^7.1 || ^8.0",
+                "psr/cache": "^1 || ^2 || ^3"
+            },
+            "require-dev": {
+                "doctrine/cache": "^1.11 || ^2.0",
+                "doctrine/coding-standard": "^6.0 || ^8.1",
+                "phpstan/phpstan": "^0.12.20",
+                "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
+                "symfony/cache": "^4.4 || ^5.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Docblock Annotations Parser",
+            "homepage": "https://www.doctrine-project.org/projects/annotations.html",
+            "keywords": [
+                "annotations",
+                "docblock",
+                "parser"
+            ],
+            "support": {
+                "issues": "https://github.com/doctrine/annotations/issues",
+                "source": "https://github.com/doctrine/annotations/tree/1.13.1"
+            },
+            "time": "2021-05-16T18:07:53+00:00"
+        },
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
+                "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^8.0",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com",
+                    "homepage": "https://ocramius.github.io/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+            "keywords": [
+                "constructor",
+                "instantiate"
+            ],
+            "support": {
+                "issues": "https://github.com/doctrine/instantiator/issues",
+                "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
+            },
+            "funding": [
+                {
+                    "url": "https://www.doctrine-project.org/sponsorship.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://www.patreon.com/phpdoctrine",
+                    "type": "patreon"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-11-10T18:47:58+00:00"
+        },
+        {
+            "name": "doctrine/lexer",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/lexer.git",
+                "reference": "e864bbf5904cb8f5bb334f99209b48018522f042"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042",
+                "reference": "e864bbf5904cb8f5bb334f99209b48018522f042",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^6.0",
+                "phpstan/phpstan": "^0.11.8",
+                "phpunit/phpunit": "^8.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
+            "keywords": [
+                "annotations",
+                "docblock",
+                "lexer",
+                "parser",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/doctrine/lexer/issues",
+                "source": "https://github.com/doctrine/lexer/tree/1.2.1"
+            },
+            "funding": [
+                {
+                    "url": "https://www.doctrine-project.org/sponsorship.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://www.patreon.com/phpdoctrine",
+                    "type": "patreon"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-05-25T17:44:05+00:00"
+        },
+        {
+            "name": "enlightn/security-checker",
+            "version": "v1.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/enlightn/security-checker.git",
+                "reference": "dc5bce653fa4d9c792e9dcffa728c0642847c1e1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/enlightn/security-checker/zipball/dc5bce653fa4d9c792e9dcffa728c0642847c1e1",
+                "reference": "dc5bce653fa4d9c792e9dcffa728c0642847c1e1",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/guzzle": "^6.3|^7.0",
+                "php": ">=5.6",
+                "symfony/console": "^3.4|^4|^5",
+                "symfony/finder": "^3|^4|^5",
+                "symfony/process": "^3.4|^4|^5",
+                "symfony/yaml": "^3.4|^4|^5"
+            },
+            "require-dev": {
+                "ext-zip": "*",
+                "friendsofphp/php-cs-fixer": "^2.18",
+                "phpunit/phpunit": "^5.5|^6|^7|^8|^9"
+            },
+            "bin": [
+                "security-checker"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Enlightn\\SecurityChecker\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paras Malhotra",
+                    "email": "paras@laravel-enlightn.com"
+                },
+                {
+                    "name": "Miguel Piedrafita",
+                    "email": "soy@miguelpiedrafita.com"
+                }
+            ],
+            "description": "A PHP dependency vulnerabilities scanner based on the Security Advisories Database.",
+            "keywords": [
+                "package",
+                "php",
+                "scanner",
+                "security",
+                "security advisories",
+                "vulnerability scanner"
+            ],
+            "support": {
+                "issues": "https://github.com/enlightn/security-checker/issues",
+                "source": "https://github.com/enlightn/security-checker/tree/v1.9.0"
+            },
+            "time": "2021-05-06T09:03:35+00:00"
+        },
+        {
+            "name": "ergebnis/composer-normalize",
+            "version": "2.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ergebnis/composer-normalize.git",
+                "reference": "d469a15b916441959446d52a0f5d3fc9f7720317"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/d469a15b916441959446d52a0f5d3fc9f7720317",
+                "reference": "d469a15b916441959446d52a0f5d3fc9f7720317",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.1.0 || ^2.0.0",
+                "ergebnis/json-normalizer": "^1.0.3",
+                "ergebnis/json-printer": "^3.1.1",
+                "justinrainbow/json-schema": "^5.2.10",
+                "localheinz/diff": "^1.1.1",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "composer/composer": "^1.10.22 || ^2.0.13",
+                "ergebnis/license": "^1.1.0",
+                "ergebnis/php-cs-fixer-config": "^2.14.0",
+                "ergebnis/phpstan-rules": "~0.15.3",
+                "ergebnis/test-util": "^1.5.0",
+                "phpstan/extension-installer": "^1.1.0",
+                "phpstan/phpstan": "~0.12.89",
+                "phpstan/phpstan-deprecation-rules": "~0.12.6",
+                "phpstan/phpstan-phpunit": "~0.12.19",
+                "phpstan/phpstan-strict-rules": "~0.12.9",
+                "phpunit/phpunit": "^8.5.16",
+                "psalm/plugin-phpunit": "~0.16.0",
+                "symfony/filesystem": "^5.3.0",
+                "vimeo/psalm": "^4.7.3"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin",
+                "composer-normalize": {
+                    "indent-size": 2,
+                    "indent-style": "space"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Ergebnis\\Composer\\Normalize\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Andreas Möller",
+                    "email": "am@localheinz.com"
+                }
+            ],
+            "description": "Provides a composer plugin for normalizing composer.json.",
+            "homepage": "https://github.com/ergebnis/composer-normalize",
+            "keywords": [
+                "composer",
+                "normalize",
+                "normalizer",
+                "plugin"
+            ],
+            "support": {
+                "issues": "https://github.com/ergebnis/composer-normalize/issues",
+                "source": "https://github.com/ergebnis/composer-normalize"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/localheinz",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-06-15T08:06:45+00:00"
+        },
+        {
+            "name": "ergebnis/json-normalizer",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ergebnis/json-normalizer.git",
+                "reference": "4a7f064ce34d5a2e382564565cdd433dbc5b9494"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/4a7f064ce34d5a2e382564565cdd433dbc5b9494",
+                "reference": "4a7f064ce34d5a2e382564565cdd433dbc5b9494",
+                "shasum": ""
+            },
+            "require": {
+                "ergebnis/json-printer": "^3.1.1",
+                "ext-json": "*",
+                "justinrainbow/json-schema": "^5.2.10",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "ergebnis/license": "^1.1.0",
+                "ergebnis/php-cs-fixer-config": "^2.10.0",
+                "ergebnis/phpstan-rules": "~0.15.3",
+                "ergebnis/test-util": "^1.4.0",
+                "infection/infection": "~0.15.3",
+                "jangregor/phpstan-prophecy": "~0.8.1",
+                "phpstan/extension-installer": "^1.1.0",
+                "phpstan/phpstan": "~0.12.80",
+                "phpstan/phpstan-deprecation-rules": "~0.12.6",
+                "phpstan/phpstan-phpunit": "~0.12.17",
+                "phpstan/phpstan-strict-rules": "~0.12.9",
+                "phpunit/phpunit": "^8.5.14",
+                "psalm/plugin-phpunit": "~0.12.2",
+                "vimeo/psalm": "^3.18"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Ergebnis\\Json\\Normalizer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Andreas Möller",
+                    "email": "am@localheinz.com"
+                }
+            ],
+            "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.",
+            "homepage": "https://github.com/ergebnis/json-normalizer",
+            "keywords": [
+                "json",
+                "normalizer"
+            ],
+            "support": {
+                "issues": "https://github.com/ergebnis/json-normalizer/issues",
+                "source": "https://github.com/ergebnis/json-normalizer"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/localheinz",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-03-06T13:33:57+00:00"
+        },
+        {
+            "name": "ergebnis/json-printer",
+            "version": "3.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ergebnis/json-printer.git",
+                "reference": "e4190dadd9937a77d8afcaf2b6c42a528ab367d6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/e4190dadd9937a77d8afcaf2b6c42a528ab367d6",
+                "reference": "e4190dadd9937a77d8afcaf2b6c42a528ab367d6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "ergebnis/license": "^1.0.0",
+                "ergebnis/php-cs-fixer-config": "^2.2.1",
+                "ergebnis/phpstan-rules": "~0.15.2",
+                "ergebnis/test-util": "^1.1.0",
+                "infection/infection": "~0.15.3",
+                "phpstan/extension-installer": "^1.0.4",
+                "phpstan/phpstan": "~0.12.40",
+                "phpstan/phpstan-deprecation-rules": "~0.12.5",
+                "phpstan/phpstan-phpunit": "~0.12.16",
+                "phpstan/phpstan-strict-rules": "~0.12.4",
+                "phpunit/phpunit": "^8.5.8",
+                "psalm/plugin-phpunit": "~0.11.0",
+                "vimeo/psalm": "^3.14.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Ergebnis\\Json\\Printer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Andreas Möller",
+                    "email": "am@localheinz.com"
+                }
+            ],
+            "description": "Provides a JSON printer, allowing for flexible indentation.",
+            "homepage": "https://github.com/ergebnis/json-printer",
+            "keywords": [
+                "formatter",
+                "json",
+                "printer"
+            ],
+            "support": {
+                "issues": "https://github.com/ergebnis/json-printer/issues",
+                "source": "https://github.com/ergebnis/json-printer"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/localheinz",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-08-30T12:17:03+00:00"
+        },
+        {
+            "name": "felixfbecker/advanced-json-rpc",
+            "version": "v3.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git",
+                "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447",
+                "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447",
+                "shasum": ""
+            },
+            "require": {
+                "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
+                "php": "^7.1 || ^8.0",
+                "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "AdvancedJsonRpc\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Felix Becker",
+                    "email": "felix.b@outlook.com"
+                }
+            ],
+            "description": "A more advanced JSONRPC implementation",
+            "support": {
+                "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues",
+                "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1"
+            },
+            "time": "2021-06-11T22:34:44+00:00"
+        },
+        {
+            "name": "felixfbecker/language-server-protocol",
+            "version": "1.5.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/felixfbecker/php-language-server-protocol.git",
+                "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
+                "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "*",
+                "squizlabs/php_codesniffer": "^3.1",
+                "vimeo/psalm": "^4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "LanguageServerProtocol\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Felix Becker",
+                    "email": "felix.b@outlook.com"
+                }
+            ],
+            "description": "PHP classes for the Language Server Protocol",
+            "keywords": [
+                "language",
+                "microsoft",
+                "php",
+                "server"
+            ],
+            "support": {
+                "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
+                "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1"
+            },
+            "time": "2021-02-22T14:02:09+00:00"
+        },
+        {
+            "name": "friendsofphp/php-cs-fixer",
+            "version": "v3.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
+                "reference": "c15377bdfa8d1ecf186f1deadec39c89984e1167"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/c15377bdfa8d1ecf186f1deadec39c89984e1167",
+                "reference": "c15377bdfa8d1ecf186f1deadec39c89984e1167",
+                "shasum": ""
+            },
+            "require": {
+                "composer/semver": "^3.2",
+                "composer/xdebug-handler": "^2.0",
+                "doctrine/annotations": "^1.12",
+                "ext-json": "*",
+                "ext-tokenizer": "*",
+                "php": "^7.1.3 || ^8.0",
+                "php-cs-fixer/diff": "^2.0",
+                "symfony/console": "^4.4.20 || ^5.1.3",
+                "symfony/event-dispatcher": "^4.4.20 || ^5.0",
+                "symfony/filesystem": "^4.4.20 || ^5.0",
+                "symfony/finder": "^4.4.20 || ^5.0",
+                "symfony/options-resolver": "^4.4.20 || ^5.0",
+                "symfony/polyfill-php72": "^1.22",
+                "symfony/process": "^4.4.20 || ^5.0",
+                "symfony/stopwatch": "^4.4.20 || ^5.0"
+            },
+            "require-dev": {
+                "justinrainbow/json-schema": "^5.2",
+                "keradus/cli-executor": "^1.4",
+                "mikey179/vfsstream": "^1.6.8",
+                "php-coveralls/php-coveralls": "^2.4.3",
+                "php-cs-fixer/accessible-object": "^1.1",
+                "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2",
+                "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1",
+                "phpspec/prophecy": "^1.10.3",
+                "phpspec/prophecy-phpunit": "^1.1 || ^2.0",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.14 || ^9.5",
+                "phpunitgoodpractices/polyfill": "^1.5",
+                "phpunitgoodpractices/traits": "^1.9.1",
+                "symfony/phpunit-bridge": "^5.2.4",
+                "symfony/yaml": "^4.4.20 || ^5.0"
+            },
+            "suggest": {
+                "ext-dom": "For handling output formats in XML",
+                "ext-mbstring": "For handling non-UTF8 characters.",
+                "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible."
+            },
+            "bin": [
+                "php-cs-fixer"
+            ],
+            "type": "application",
+            "autoload": {
+                "psr-4": {
+                    "PhpCsFixer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Dariusz Rumiński",
+                    "email": "dariusz.ruminski@gmail.com"
+                }
+            ],
+            "description": "A tool to automatically fix PHP code style",
+            "support": {
+                "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
+                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.0.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/keradus",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-05-03T21:51:58+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "6.5.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.0",
+                "guzzlehttp/psr7": "^1.6.1",
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.17.0"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+                "psr/log": "^1.1"
+            },
+            "suggest": {
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.5-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "homepage": "http://guzzlephp.org/",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/6.5"
+            },
+            "time": "2020-06-16T21:01:06+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "1.4.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
+                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "^4.4 || ^5.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/1.4.1"
+            },
+            "time": "2021-03-07T09:25:29+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "1.8.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "dc960a912984efb74d0a90222870c72c87f10c91"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
+                "reference": "dc960a912984efb74d0a90222870c72c87f10c91",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0",
+                "psr/http-message": "~1.0",
+                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+            },
+            "provide": {
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "ext-zlib": "*",
+                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.7-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/1.8.2"
+            },
+            "time": "2021-04-26T09:17:50+00:00"
+        },
+        {
+            "name": "insolita/unused-scanner",
+            "version": "2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Insolita/unused-scanner.git",
+                "reference": "c52f871b138f24a8d98e000ca9f02b51f81fa900"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Insolita/unused-scanner/zipball/c52f871b138f24a8d98e000ca9f02b51f81fa900",
+                "reference": "c52f871b138f24a8d98e000ca9f02b51f81fa900",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "php": ">=7.1",
+                "symfony/finder": "^3.4|^4.0|^5.0"
+            },
+            "require-dev": {
+                "php-mock/php-mock-phpunit": "^2.6",
+                "phpunit/phpunit": "^7.0|^8.0|^9.0",
+                "symfony/thanks": "^1.2"
+            },
+            "bin": [
+                "unused_scanner"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-legacy": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "insolita\\Scanner\\Lib\\": "Lib",
+                    "insolita\\Scanner\\Exceptions\\": "Exceptions",
+                    "tests\\": "tests"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "mit"
+            ],
+            "authors": [
+                {
+                    "name": "insolita",
+                    "email": "webmaster100500@ya.ru"
+                }
+            ],
+            "description": "Detect unused composer dependencies",
+            "support": {
+                "issues": "https://github.com/Insolita/unused-scanner/issues",
+                "source": "https://github.com/Insolita/unused-scanner/tree/2.3.0"
+            },
+            "time": "2021-04-15T15:59:31+00:00"
+        },
+        {
+            "name": "justinrainbow/json-schema",
+            "version": "5.2.10",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/justinrainbow/json-schema.git",
+                "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+                "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+                "json-schema/json-schema-test-suite": "1.2.0",
+                "phpunit/phpunit": "^4.8.35"
+            },
+            "bin": [
+                "bin/validate-json"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "JsonSchema\\": "src/JsonSchema/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bruno Prieto Reis",
+                    "email": "bruno.p.reis@gmail.com"
+                },
+                {
+                    "name": "Justin Rainbow",
+                    "email": "justin.rainbow@gmail.com"
+                },
+                {
+                    "name": "Igor Wiedler",
+                    "email": "igor@wiedler.ch"
+                },
+                {
+                    "name": "Robert Schönthal",
+                    "email": "seroscho@googlemail.com"
+                }
+            ],
+            "description": "A library to validate a json schema.",
+            "homepage": "https://github.com/justinrainbow/json-schema",
+            "keywords": [
+                "json",
+                "schema"
+            ],
+            "support": {
+                "issues": "https://github.com/justinrainbow/json-schema/issues",
+                "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10"
+            },
+            "time": "2020-05-27T16:41:55+00:00"
+        },
+        {
+            "name": "league/climate",
+            "version": "3.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/climate.git",
+                "reference": "61eecbde7645528a855a3afef65c5354093b27b2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/climate/zipball/61eecbde7645528a855a3afef65c5354093b27b2",
+                "reference": "61eecbde7645528a855a3afef65c5354093b27b2",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2",
+                "psr/log": "^1.0",
+                "seld/cli-prompt": "^1.0"
+            },
+            "require-dev": {
+                "mikey179/vfsstream": "^1.4",
+                "mockery/mockery": "^1.0",
+                "phpunit/phpunit": "^5.7.16"
+            },
+            "suggest": {
+                "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\CLImate\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Joe Tannenbaum",
+                    "email": "hey@joe.codes",
+                    "homepage": "http://joe.codes/",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Craig Duncan",
+                    "email": "git@duncanc.co.uk",
+                    "homepage": "https://github.com/duncan3dc",
+                    "role": "Developer"
+                }
+            ],
+            "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.",
+            "keywords": [
+                "cli",
+                "colors",
+                "command",
+                "php",
+                "terminal"
+            ],
+            "support": {
+                "issues": "https://github.com/thephpleague/climate/issues",
+                "source": "https://github.com/thephpleague/climate/tree/3.6.0"
+            },
+            "time": "2020-10-04T16:02:59+00:00"
+        },
+        {
+            "name": "localheinz/diff",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/localheinz/diff.git",
+                "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/localheinz/diff/zipball/851bb20ea8358c86f677f5f111c4ab031b1c764c",
+                "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "symfony/process": "^4.2 || ^5"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                }
+            ],
+            "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize",
+            "homepage": "https://github.com/localheinz/diff",
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "support": {
+                "source": "https://github.com/localheinz/diff/tree/main"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-07-06T04:49:32+00:00"
+        },
+        {
+            "name": "myclabs/deep-copy",
+            "version": "1.10.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/myclabs/DeepCopy.git",
+                "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
+                "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "replace": {
+                "myclabs/deep-copy": "self.version"
+            },
+            "require-dev": {
+                "doctrine/collections": "^1.0",
+                "doctrine/common": "^2.6",
+                "phpunit/phpunit": "^7.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "DeepCopy\\": "src/DeepCopy/"
+                },
+                "files": [
+                    "src/DeepCopy/deep_copy.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Create deep copies (clones) of your objects",
+            "keywords": [
+                "clone",
+                "copy",
+                "duplicate",
+                "object",
+                "object graph"
+            ],
+            "support": {
+                "issues": "https://github.com/myclabs/DeepCopy/issues",
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
+            },
+            "funding": [
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-11-13T09:40:50+00:00"
+        },
+        {
+            "name": "netresearch/jsonmapper",
+            "version": "v4.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/cweiske/jsonmapper.git",
+                "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d",
+                "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-pcre": "*",
+                "ext-reflection": "*",
+                "ext-spl": "*",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0",
+                "squizlabs/php_codesniffer": "~3.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "JsonMapper": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "OSL-3.0"
+            ],
+            "authors": [
+                {
+                    "name": "Christian Weiske",
+                    "email": "cweiske@cweiske.de",
+                    "homepage": "http://github.com/cweiske/jsonmapper/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Map nested JSON structures onto PHP classes",
+            "support": {
+                "email": "cweiske@cweiske.de",
+                "issues": "https://github.com/cweiske/jsonmapper/issues",
+                "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0"
+            },
+            "time": "2020-12-01T19:48:11+00:00"
+        },
+        {
+            "name": "nikic/php-parser",
+            "version": "v4.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nikic/PHP-Parser.git",
+                "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/fe14cf3672a149364fb66dfe11bf6549af899f94",
+                "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "ircmaxell/php-yacc": "^0.0.7",
+                "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+            },
+            "bin": [
+                "bin/php-parse"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpParser\\": "lib/PhpParser"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Nikita Popov"
+                }
+            ],
+            "description": "A PHP parser written in PHP",
+            "keywords": [
+                "parser",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/nikic/PHP-Parser/issues",
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.11.0"
+            },
+            "time": "2021-07-03T13:36:55+00:00"
+        },
+        {
+            "name": "openlss/lib-array2xml",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nullivex/lib-array2xml.git",
+                "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90",
+                "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "LSS": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Bryan Tong",
+                    "email": "bryan@nullivex.com",
+                    "homepage": "https://www.nullivex.com"
+                },
+                {
+                    "name": "Tony Butler",
+                    "email": "spudz76@gmail.com",
+                    "homepage": "https://www.nullivex.com"
+                }
+            ],
+            "description": "Array2XML conversion library credit to lalit.org",
+            "homepage": "https://www.nullivex.com",
+            "keywords": [
+                "array",
+                "array conversion",
+                "xml",
+                "xml conversion"
+            ],
+            "support": {
+                "issues": "https://github.com/nullivex/lib-array2xml/issues",
+                "source": "https://github.com/nullivex/lib-array2xml/tree/master"
+            },
+            "time": "2019-03-29T20:06:56+00:00"
+        },
+        {
+            "name": "pdepend/pdepend",
+            "version": "2.9.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/pdepend/pdepend.git",
+                "reference": "1632f0cee84512ffd6dde71e58536b3b06528c41"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1632f0cee84512ffd6dde71e58536b3b06528c41",
+                "reference": "1632f0cee84512ffd6dde71e58536b3b06528c41",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.7",
+                "symfony/config": "^2.3.0|^3|^4|^5",
+                "symfony/dependency-injection": "^2.3.0|^3|^4|^5",
+                "symfony/filesystem": "^2.3.0|^3|^4|^5"
+            },
+            "require-dev": {
+                "easy-doc/easy-doc": "0.0.0|^1.2.3",
+                "gregwar/rst": "^1.0",
+                "phpunit/phpunit": "^4.8.36|^5.7.27",
+                "squizlabs/php_codesniffer": "^2.0.0"
+            },
+            "bin": [
+                "src/bin/pdepend"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PDepend\\": "src/main/php/PDepend"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Official version of pdepend to be handled with Composer",
+            "support": {
+                "issues": "https://github.com/pdepend/pdepend/issues",
+                "source": "https://github.com/pdepend/pdepend/tree/2.9.1"
+            },
+            "funding": [
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-04-15T21:36:28+00:00"
+        },
+        {
+            "name": "phar-io/manifest",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/manifest.git",
+                "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+                "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-phar": "*",
+                "ext-xmlwriter": "*",
+                "phar-io/version": "^3.0.1",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+            "support": {
+                "issues": "https://github.com/phar-io/manifest/issues",
+                "source": "https://github.com/phar-io/manifest/tree/master"
+            },
+            "time": "2020-06-27T14:33:11+00:00"
+        },
+        {
+            "name": "phar-io/version",
+            "version": "3.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/version.git",
+                "reference": "bae7c545bef187884426f042434e561ab1ddb182"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
+                "reference": "bae7c545bef187884426f042434e561ab1ddb182",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Library for handling version information and constraints",
+            "support": {
+                "issues": "https://github.com/phar-io/version/issues",
+                "source": "https://github.com/phar-io/version/tree/3.1.0"
+            },
+            "time": "2021-02-23T14:00:09+00:00"
+        },
+        {
+            "name": "php-cs-fixer/diff",
+            "version": "v2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHP-CS-Fixer/diff.git",
+                "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3",
+                "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
+                "symfony/process": "^3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                }
+            ],
+            "description": "sebastian/diff v3 backport support for PHP 5.6+",
+            "homepage": "https://github.com/PHP-CS-Fixer",
+            "keywords": [
+                "diff"
+            ],
+            "support": {
+                "issues": "https://github.com/PHP-CS-Fixer/diff/issues",
+                "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2"
+            },
+            "time": "2020-10-14T08:32:19+00:00"
+        },
+        {
+            "name": "php-parallel-lint/php-parallel-lint",
+            "version": "v1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git",
+                "reference": "772a954e5f119f6f5871d015b23eabed8cbdadfb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/772a954e5f119f6f5871d015b23eabed8cbdadfb",
+                "reference": "772a954e5f119f6f5871d015b23eabed8cbdadfb",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": ">=5.3.0"
+            },
+            "replace": {
+                "grogy/php-parallel-lint": "*",
+                "jakub-onderka/php-parallel-lint": "*"
+            },
+            "require-dev": {
+                "nette/tester": "^1.3 || ^2.0",
+                "php-parallel-lint/php-console-highlighter": "~0.3",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "suggest": {
+                "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet"
+            },
+            "bin": [
+                "parallel-lint"
+            ],
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "./"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jakub Onderka",
+                    "email": "ahoj@jakubonderka.cz"
+                }
+            ],
+            "description": "This tool check syntax of PHP files about 20x faster than serial check.",
+            "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
+            "support": {
+                "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues",
+                "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.3.0"
+            },
+            "time": "2021-04-07T14:42:48+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-common",
+            "version": "2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-2.x": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "opensource@ijaap.nl"
+                }
+            ],
+            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+            "homepage": "http://www.phpdoc.org",
+            "keywords": [
+                "FQSEN",
+                "phpDocumentor",
+                "phpdoc",
+                "reflection",
+                "static analysis"
+            ],
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+            },
+            "time": "2020-06-27T09:03:43+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-docblock",
+            "version": "5.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+                "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
+                "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
+                "shasum": ""
+            },
+            "require": {
+                "ext-filter": "*",
+                "php": "^7.2 || ^8.0",
+                "phpdocumentor/reflection-common": "^2.2",
+                "phpdocumentor/type-resolver": "^1.3",
+                "webmozart/assert": "^1.9.1"
+            },
+            "require-dev": {
+                "mockery/mockery": "~1.3.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "me@mikevanriel.com"
+                },
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "account@ijaap.nl"
+                }
+            ],
+            "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+            },
+            "time": "2020-09-03T19:13:55+00:00"
+        },
+        {
+            "name": "phpdocumentor/type-resolver",
+            "version": "1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/TypeResolver.git",
+                "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+                "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "phpdocumentor/reflection-common": "^2.0"
+            },
+            "require-dev": {
+                "ext-tokenizer": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-1.x": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "me@mikevanriel.com"
+                }
+            ],
+            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+            "support": {
+                "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
+            },
+            "time": "2020-09-17T18:55:26+00:00"
+        },
+        {
+            "name": "phpmd/phpmd",
+            "version": "2.10.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpmd/phpmd.git",
+                "reference": "bd5ef43d1dcaf7272605027c959c1c5ff3761f7a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpmd/phpmd/zipball/bd5ef43d1dcaf7272605027c959c1c5ff3761f7a",
+                "reference": "bd5ef43d1dcaf7272605027c959c1c5ff3761f7a",
+                "shasum": ""
+            },
+            "require": {
+                "composer/xdebug-handler": "^1.0 || ^2.0",
+                "ext-xml": "*",
+                "pdepend/pdepend": "^2.9.1",
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "easy-doc/easy-doc": "0.0.0 || ^1.3.2",
+                "ext-json": "*",
+                "ext-simplexml": "*",
+                "gregwar/rst": "^1.0",
+                "mikey179/vfsstream": "^1.6.8",
+                "phpunit/phpunit": "^4.8.36 || ^5.7.27",
+                "squizlabs/php_codesniffer": "^2.0"
+            },
+            "bin": [
+                "src/bin/phpmd"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "PHPMD\\": "src/main/php"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Manuel Pichler",
+                    "email": "github@manuel-pichler.de",
+                    "homepage": "https://github.com/manuelpichler",
+                    "role": "Project Founder"
+                },
+                {
+                    "name": "Marc Würth",
+                    "email": "ravage@bluewin.ch",
+                    "homepage": "https://github.com/ravage84",
+                    "role": "Project Maintainer"
+                },
+                {
+                    "name": "Other contributors",
+                    "homepage": "https://github.com/phpmd/phpmd/graphs/contributors",
+                    "role": "Contributors"
+                }
+            ],
+            "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
+            "homepage": "https://phpmd.org/",
+            "keywords": [
+                "mess detection",
+                "mess detector",
+                "pdepend",
+                "phpmd",
+                "pmd"
+            ],
+            "support": {
+                "irc": "irc://irc.freenode.org/phpmd",
+                "issues": "https://github.com/phpmd/phpmd/issues",
+                "source": "https://github.com/phpmd/phpmd/tree/2.10.1"
+            },
+            "funding": [
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-11T17:16:16+00:00"
+        },
+        {
+            "name": "phpspec/prophecy",
+            "version": "1.13.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpspec/prophecy.git",
+                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
+                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.2",
+                "php": "^7.2 || ~8.0, <8.1",
+                "phpdocumentor/reflection-docblock": "^5.2",
+                "sebastian/comparator": "^3.0 || ^4.0",
+                "sebastian/recursion-context": "^3.0 || ^4.0"
+            },
+            "require-dev": {
+                "phpspec/phpspec": "^6.0",
+                "phpunit/phpunit": "^8.0 || ^9.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.11.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Prophecy\\": "src/Prophecy"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "ever.zet@gmail.com",
+                    "homepage": "http://everzet.com"
+                },
+                {
+                    "name": "Marcello Duarte",
+                    "email": "marcello.duarte@gmail.com"
+                }
+            ],
+            "description": "Highly opinionated mocking framework for PHP 5.3+",
+            "homepage": "https://github.com/phpspec/prophecy",
+            "keywords": [
+                "Double",
+                "Dummy",
+                "fake",
+                "mock",
+                "spy",
+                "stub"
+            ],
+            "support": {
+                "issues": "https://github.com/phpspec/prophecy/issues",
+                "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
+            },
+            "time": "2021-03-17T13:42:18+00:00"
+        },
+        {
+            "name": "phpstan/phpstan",
+            "version": "0.12.92",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpstan/phpstan.git",
+                "reference": "64d4c5dc8ea96972bc18432d137a330239a5d2b2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/64d4c5dc8ea96972bc18432d137a330239a5d2b2",
+                "reference": "64d4c5dc8ea96972bc18432d137a330239a5d2b2",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1|^8.0"
+            },
+            "conflict": {
+                "phpstan/phpstan-shim": "*"
+            },
+            "bin": [
+                "phpstan",
+                "phpstan.phar"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.12-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "PHPStan - PHP Static Analysis Tool",
+            "support": {
+                "issues": "https://github.com/phpstan/phpstan/issues",
+                "source": "https://github.com/phpstan/phpstan/tree/0.12.92"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ondrejmirtes",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/phpstan",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/phpstan",
+                    "type": "patreon"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-07-10T13:53:49+00:00"
+        },
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "7.0.14",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+                "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c",
+                "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-xmlwriter": "*",
+                "php": ">=7.2",
+                "phpunit/php-file-iterator": "^2.0.2",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-token-stream": "^3.1.1 || ^4.0",
+                "sebastian/code-unit-reverse-lookup": "^1.0.1",
+                "sebastian/environment": "^4.2.2",
+                "sebastian/version": "^2.0.1",
+                "theseer/tokenizer": "^1.1.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.2.2"
+            },
+            "suggest": {
+                "ext-xdebug": "^2.7.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "7.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-12-02T13:39:03+00:00"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "2.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357",
+                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:25:21+00:00"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-text-template.git",
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+            "keywords": [
+                "template"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+                "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
+            },
+            "time": "2015-06-21T13:50:34+00:00"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "2.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-timer.git",
+                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "https://github.com/sebastianbergmann/php-timer/",
+            "keywords": [
+                "timer"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+                "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:20:02+00:00"
+        },
+        {
+            "name": "phpunit/php-token-stream",
+            "version": "3.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
+                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2",
+                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Wrapper around PHP's tokenizer extension.",
+            "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
+            "keywords": [
+                "tokenizer"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
+                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "abandoned": true,
+            "time": "2020-11-30T08:38:46+00:00"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "8.5.17",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git",
+                "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/79067856d85421c56d413bd238d4e2cd6b0e54da",
+                "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.3.1",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-xml": "*",
+                "ext-xmlwriter": "*",
+                "myclabs/deep-copy": "^1.10.0",
+                "phar-io/manifest": "^2.0.1",
+                "phar-io/version": "^3.0.2",
+                "php": ">=7.2",
+                "phpspec/prophecy": "^1.10.3",
+                "phpunit/php-code-coverage": "^7.0.12",
+                "phpunit/php-file-iterator": "^2.0.2",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-timer": "^2.1.2",
+                "sebastian/comparator": "^3.0.2",
+                "sebastian/diff": "^3.0.2",
+                "sebastian/environment": "^4.2.3",
+                "sebastian/exporter": "^3.1.2",
+                "sebastian/global-state": "^3.0.0",
+                "sebastian/object-enumerator": "^3.0.3",
+                "sebastian/resource-operations": "^2.0.1",
+                "sebastian/type": "^1.1.3",
+                "sebastian/version": "^2.0.1"
+            },
+            "require-dev": {
+                "ext-pdo": "*"
+            },
+            "suggest": {
+                "ext-soap": "*",
+                "ext-xdebug": "*",
+                "phpunit/php-invoker": "^2.0.0"
+            },
+            "bin": [
+                "phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "8.5-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "https://phpunit.de/",
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.17"
+            },
+            "funding": [
+                {
+                    "url": "https://phpunit.de/donate.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-06-23T05:12:43+00:00"
+        },
+        {
+            "name": "psr/cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/cache/tree/master"
+            },
+            "time": "2016-08-06T20:24:11+00:00"
+        },
+        {
+            "name": "psr/container",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
+                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "support": {
+                "issues": "https://github.com/php-fig/container/issues",
+                "source": "https://github.com/php-fig/container/tree/1.1.1"
+            },
+            "time": "2021-03-05T17:36:06+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/master"
+            },
+            "time": "2016-08-06T14:39:51+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/1.1.4"
+            },
+            "time": "2021-05-03T11:20:27+00:00"
+        },
+        {
+            "name": "ralouphie/getallheaders",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/getallheaders.git",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/getallheaders.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "ralph.khattar@gmail.com"
+                }
+            ],
+            "description": "A polyfill for getallheaders.",
+            "support": {
+                "issues": "https://github.com/ralouphie/getallheaders/issues",
+                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+            },
+            "time": "2019-03-08T08:55:37+00:00"
+        },
+        {
+            "name": "roave/security-advisories",
+            "version": "dev-latest",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Roave/SecurityAdvisories.git",
+                "reference": "1a08d0c7ab47e57fad0d254951a615f07445e91f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/1a08d0c7ab47e57fad0d254951a615f07445e91f",
+                "reference": "1a08d0c7ab47e57fad0d254951a615f07445e91f",
+                "shasum": ""
+            },
+            "conflict": {
+                "3f/pygmentize": "<1.2",
+                "adodb/adodb-php": "<5.20.12",
+                "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
+                "amazing/media2click": ">=1,<1.3.3",
+                "amphp/artax": "<1.0.6|>=2,<2.0.6",
+                "amphp/http": "<1.0.1",
+                "amphp/http-client": ">=4,<4.4",
+                "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
+                "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
+                "aws/aws-sdk-php": ">=3,<3.2.1",
+                "bagisto/bagisto": "<0.1.5",
+                "barrelstrength/sprout-base-email": "<1.2.7",
+                "barrelstrength/sprout-forms": "<3.9",
+                "baserproject/basercms": "<4.4.5",
+                "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
+                "bolt/bolt": "<3.7.2",
+                "bolt/core": "<4.1.13",
+                "brightlocal/phpwhois": "<=4.2.5",
+                "buddypress/buddypress": "<5.1.2",
+                "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
+                "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
+                "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
+                "cartalyst/sentry": "<=2.1.6",
+                "centreon/centreon": "<20.10.7",
+                "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
+                "codeigniter/framework": "<=3.0.6",
+                "composer/composer": "<1.10.22|>=2-alpha.1,<2.0.13",
+                "contao-components/mediaelement": ">=2.14.2,<2.21.1",
+                "contao/core": ">=2,<3.5.39",
+                "contao/core-bundle": ">=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|= 4.10.0",
+                "contao/listing-bundle": ">=4,<4.4.8",
+                "craftcms/cms": "<3.6.7",
+                "croogo/croogo": "<3.0.7",
+                "datadog/dd-trace": ">=0.30,<0.30.2",
+                "david-garcia/phpwhois": "<=4.3.1",
+                "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1",
+                "directmailteam/direct-mail": "<5.2.4",
+                "doctrine/annotations": ">=1,<1.2.7",
+                "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2",
+                "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1",
+                "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2",
+                "doctrine/doctrine-bundle": "<1.5.2",
+                "doctrine/doctrine-module": "<=0.7.1",
+                "doctrine/mongodb-odm": ">=1,<1.0.2",
+                "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
+                "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
+                "dolibarr/dolibarr": "<14",
+                "dompdf/dompdf": ">=0.6,<0.6.2",
+                "drupal/core": ">=7,<7.80|>=8,<8.9.14|>=9,<9.0.12|>=9.1,<9.1.7",
+                "drupal/drupal": ">=7,<7.80|>=8,<8.9.14|>=9,<9.0.12|>=9.1,<9.1.7",
+                "dweeves/magmi": "<=0.7.24",
+                "endroid/qr-code-bundle": "<3.4.2",
+                "enshrined/svg-sanitize": "<0.13.1",
+                "erusev/parsedown": "<1.7.2",
+                "ether/logs": "<3.0.4",
+                "ezsystems/demobundle": ">=5.4,<5.4.6.1",
+                "ezsystems/ez-support-tools": ">=2.2,<2.2.3",
+                "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
+                "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
+                "ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
+                "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
+                "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
+                "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<=1.3.1",
+                "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<=1.3.1",
+                "ezsystems/ezplatform-user": ">=1,<1.0.1",
+                "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<=7.5.15.1",
+                "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.2|>=2011,<2017.12.7.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.5.1",
+                "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
+                "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
+                "ezyang/htmlpurifier": "<4.1.1",
+                "facade/ignition": "<1.16.14|>=2,<2.4.2|>=2.5,<2.5.2",
+                "feehi/cms": "<=2.1.1",
+                "firebase/php-jwt": "<2",
+                "flarum/core": ">=1,<=1.0.1",
+                "flarum/sticky": ">=0.1-beta.14,<=0.1-beta.15",
+                "flarum/tags": "<=0.1-beta.13",
+                "fluidtypo3/vhs": "<5.1.1",
+                "fooman/tcpdf": "<6.2.22",
+                "forkcms/forkcms": "<5.8.3",
+                "fossar/tcpdf-parser": "<6.2.22",
+                "francoisjacquet/rosariosis": "<6.5.1",
+                "friendsofsymfony/oauth2-php": "<1.3",
+                "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
+                "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
+                "friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
+                "froala/wysiwyg-editor": "<3.2.7",
+                "fuel/core": "<1.8.1",
+                "getgrav/grav": "<=1.7.10",
+                "getkirby/cms": "<=3.5.6",
+                "getkirby/panel": "<2.5.14",
+                "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
+                "gree/jose": "<=2.2",
+                "gregwar/rst": "<1.0.3",
+                "grumpydictator/firefly-iii": "<5.5.13",
+                "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
+                "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10",
+                "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4",
+                "illuminate/database": "<6.20.26|>=7,<8.40",
+                "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
+                "illuminate/view": ">=7,<7.1.2",
+                "impresscms/impresscms": "<=1.4.2",
+                "intelliants/subrion": "<=4.2.1",
+                "ivankristianto/phpwhois": "<=4.3",
+                "james-heinrich/getid3": "<1.9.9",
+                "joomla/archive": "<1.1.10",
+                "joomla/session": "<1.3.1",
+                "jsmitty12/phpwhois": "<5.1",
+                "kazist/phpwhois": "<=4.2.6",
+                "kitodo/presentation": "<3.1.2",
+                "klaviyo/magento2-extension": ">=1,<3",
+                "kreait/firebase-php": ">=3.2,<3.8.1",
+                "la-haute-societe/tcpdf": "<6.2.22",
+                "laminas/laminas-http": "<2.14.2",
+                "laravel/framework": "<6.20.26|>=7,<8.40",
+                "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
+                "lavalite/cms": "<=5.8",
+                "league/commonmark": "<0.18.3",
+                "league/flysystem": "<1.1.4|>=2,<2.1.1",
+                "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
+                "librenms/librenms": "<21.1",
+                "livewire/livewire": ">2.2.4,<2.2.6",
+                "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2",
+                "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
+                "magento/magento1ce": "<1.9.4.3",
+                "magento/magento1ee": ">=1,<1.14.4.3",
+                "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
+                "marcwillmann/turn": "<0.3.3",
+                "mautic/core": "<3.3.2|= 2.13.1",
+                "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35",
+                "mittwald/typo3_forum": "<1.2.1",
+                "monolog/monolog": ">=1.8,<1.12",
+                "moodle/moodle": "<3.5.17|>=3.7,<3.7.9|>=3.8,<3.8.8|>=3.9,<3.9.5|>=3.10,<3.10.2",
+                "namshi/jose": "<2.2",
+                "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
+                "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3",
+                "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
+                "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5",
+                "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
+                "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
+                "nilsteampassnet/teampass": "<=2.1.27.36",
+                "nukeviet/nukeviet": "<4.3.4",
+                "nystudio107/craft-seomatic": "<3.3",
+                "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
+                "october/backend": "<1.1.2",
+                "october/cms": "= 1.1.1|= 1.0.471|= 1.0.469|>=1.0.319,<1.0.469",
+                "october/october": ">=1.0.319,<1.0.466",
+                "october/rain": "<1.0.472|>=1.1,<1.1.2",
+                "onelogin/php-saml": "<2.10.4",
+                "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
+                "opencart/opencart": "<=3.0.3.2",
+                "openid/php-openid": "<2.3",
+                "openmage/magento-lts": "<=19.4.12|>=20,<=20.0.8",
+                "orchid/platform": ">=9,<9.4.4",
+                "oro/crm": ">=1.7,<1.7.4",
+                "oro/platform": ">=1.7,<1.7.4",
+                "padraic/humbug_get_contents": "<1.1.2",
+                "pagarme/pagarme-php": ">=0,<3",
+                "pagekit/pagekit": "<=1.0.18",
+                "paragonie/random_compat": "<2",
+                "passbolt/passbolt_api": "<2.11",
+                "paypal/merchant-sdk-php": "<3.12",
+                "pear/archive_tar": "<1.4.14",
+                "personnummer/personnummer": "<3.0.2",
+                "phanan/koel": "<5.1.4",
+                "phpfastcache/phpfastcache": ">=5,<5.0.13",
+                "phpmailer/phpmailer": "<6.5",
+                "phpmussel/phpmussel": ">=1,<1.6",
+                "phpmyadmin/phpmyadmin": "<4.9.6|>=5,<5.0.3",
+                "phpoffice/phpexcel": "<1.8.2",
+                "phpoffice/phpspreadsheet": "<1.16",
+                "phpseclib/phpseclib": "<2.0.31|>=3,<3.0.7",
+                "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
+                "phpwhois/phpwhois": "<=4.2.5",
+                "phpxmlrpc/extras": "<0.6.1",
+                "pimcore/pimcore": "<10.0.7",
+                "pocketmine/pocketmine-mp": "<3.15.4",
+                "pressbooks/pressbooks": "<5.18",
+                "prestashop/autoupgrade": ">=4,<4.10.1",
+                "prestashop/contactform": ">1.0.1,<4.3",
+                "prestashop/gamification": "<2.3.2",
+                "prestashop/productcomments": ">=4,<4.2.1",
+                "prestashop/ps_emailsubscription": "<2.6.1",
+                "prestashop/ps_facetedsearch": "<3.4.1",
+                "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
+                "propel/propel": ">=2-alpha.1,<=2-alpha.7",
+                "propel/propel1": ">=1,<=1.7.1",
+                "pterodactyl/panel": "<0.7.19|>=1-rc.0,<=1-rc.6",
+                "pusher/pusher-php-server": "<2.2.1",
+                "pwweb/laravel-core": "<=0.3.6-beta",
+                "rainlab/debugbar-plugin": "<3.1",
+                "rmccue/requests": ">=1.6,<1.8",
+                "robrichards/xmlseclibs": "<3.0.4",
+                "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1",
+                "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
+                "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
+                "sensiolabs/connect": "<4.2.3",
+                "serluck/phpwhois": "<=4.2.6",
+                "shopware/core": "<=6.4.1",
+                "shopware/platform": "<=6.4.1",
+                "shopware/production": "<=6.3.5.2",
+                "shopware/shopware": "<=5.6.9",
+                "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
+                "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
+                "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
+                "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
+                "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
+                "silverstripe/framework": "<4.7.4",
+                "silverstripe/graphql": "<=3.5|>=4-alpha.1,<4-alpha.2",
+                "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
+                "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
+                "silverstripe/subsites": ">=2,<2.1.1",
+                "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
+                "silverstripe/userforms": "<3",
+                "simple-updates/phpwhois": "<=1",
+                "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
+                "simplesamlphp/simplesamlphp": "<1.18.6",
+                "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
+                "simplito/elliptic-php": "<1.0.6",
+                "slim/slim": "<2.6",
+                "smarty/smarty": "<3.1.39",
+                "socalnick/scn-social-auth": "<1.15.2",
+                "socialiteproviders/steam": "<1.1",
+                "spoonity/tcpdf": "<6.2.22",
+                "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
+                "ssddanbrown/bookstack": "<0.29.2",
+                "stormpath/sdk": ">=0,<9.9.99",
+                "studio-42/elfinder": "<2.1.59",
+                "sulu/sulu": "<1.6.41|>=2,<2.0.10|>=2.1,<2.1.1",
+                "swiftmailer/swiftmailer": ">=4,<5.4.5",
+                "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
+                "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
+                "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
+                "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
+                "sylius/sylius": "<1.6.9|>=1.7,<1.7.9|>=1.8,<1.8.3|>=1.9,<1.9.5",
+                "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
+                "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4",
+                "symbiote/silverstripe-versionedfiles": "<=2.0.3",
+                "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+                "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+                "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
+                "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
+                "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+                "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
+                "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
+                "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
+                "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1",
+                "symfony/mime": ">=4.3,<4.3.8",
+                "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+                "symfony/polyfill": ">=1,<1.10",
+                "symfony/polyfill-php55": ">=1,<1.10",
+                "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+                "symfony/routing": ">=2,<2.0.19",
+                "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8",
+                "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
+                "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9",
+                "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
+                "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
+                "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8|>=5.3,<5.3.2",
+                "symfony/serializer": ">=2,<2.0.11",
+                "symfony/symfony": ">=2,<3.4.49|>=4,<4.4.24|>=5,<5.2.9|>=5.3,<5.3.2",
+                "symfony/translation": ">=2,<2.0.17",
+                "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
+                "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
+                "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4",
+                "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7",
+                "t3/dce": ">=2.2,<2.6.2",
+                "t3g/svg-sanitizer": "<1.0.3",
+                "tecnickcom/tcpdf": "<6.2.22",
+                "thelia/backoffice-default-template": ">=2.1,<2.1.2",
+                "thelia/thelia": ">=2.1-beta.1,<2.1.3",
+                "theonedemon/phpwhois": "<=4.2.5",
+                "titon/framework": ">=0,<9.9.99",
+                "topthink/think": "<=6.0.9",
+                "tribalsystems/zenario": "<8.8.53370",
+                "truckersmp/phpwhois": "<=4.3.1",
+                "twig/twig": "<1.38|>=2,<2.7",
+                "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.38|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
+                "typo3/cms-backend": ">=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
+                "typo3/cms-core": ">=6.2,<=6.2.56|>=7,<7.6.52|>=8,<8.7.41|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
+                "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
+                "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
+                "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
+                "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
+                "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5",
+                "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10",
+                "ua-parser/uap-php": "<3.8",
+                "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
+                "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
+                "vrana/adminer": "<4.7.9",
+                "wallabag/tcpdf": "<6.2.22",
+                "wikimedia/parsoid": "<0.12.2",
+                "willdurand/js-translation-bundle": "<2.1.1",
+                "wp-cli/wp-cli": "<2.5",
+                "yidashi/yii2cmf": "<=2",
+                "yii2mod/yii2-cms": "<1.9.2",
+                "yiisoft/yii": ">=1.1.14,<1.1.15",
+                "yiisoft/yii2": "<2.0.38",
+                "yiisoft/yii2-bootstrap": "<2.0.4",
+                "yiisoft/yii2-dev": "<2.0.15",
+                "yiisoft/yii2-elasticsearch": "<2.0.5",
+                "yiisoft/yii2-gii": "<2.0.4",
+                "yiisoft/yii2-jui": "<2.0.4",
+                "yiisoft/yii2-redis": "<2.0.8",
+                "yoast-seo-for-typo3/yoast_seo": "<7.2.1",
+                "yourls/yourls": "<1.7.4",
+                "zendesk/zendesk_api_client_php": "<2.2.11",
+                "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
+                "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
+                "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
+                "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5",
+                "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3",
+                "zendframework/zend-diactoros": ">=1,<1.8.4",
+                "zendframework/zend-feed": ">=1,<2.10.3",
+                "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1",
+                "zendframework/zend-http": ">=1,<2.8.1",
+                "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6",
+                "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3",
+                "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2",
+                "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1",
+                "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4",
+                "zendframework/zend-validator": ">=2.3,<2.3.6",
+                "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1",
+                "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6",
+                "zendframework/zendframework": "<=3",
+                "zendframework/zendframework1": "<1.12.20",
+                "zendframework/zendopenid": ">=2,<2.0.2",
+                "zendframework/zendxml": ">=1,<1.0.1",
+                "zetacomponents/mail": "<1.8.2",
+                "zf-commons/zfc-user": "<1.2.2",
+                "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
+                "zfr/zfr-oauth2-server-module": "<0.1.2",
+                "zoujingli/thinkadmin": "<6.0.22"
+            },
+            "default-branch": true,
+            "type": "metapackage",
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com",
+                    "role": "maintainer"
+                },
+                {
+                    "name": "Ilya Tribusean",
+                    "email": "slash3b@gmail.com",
+                    "role": "maintainer"
+                }
+            ],
+            "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
+            "support": {
+                "issues": "https://github.com/Roave/SecurityAdvisories/issues",
+                "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Ocramius",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-08-11T16:04:32+00:00"
+        },
+        {
+            "name": "rskuipers/php-assumptions",
+            "version": "0.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/rskuipers/php-assumptions.git",
+                "reference": "10f82ca2a268d6624553d2de4659ed6451d9ac60"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/rskuipers/php-assumptions/zipball/10f82ca2a268d6624553d2de4659ed6451d9ac60",
+                "reference": "10f82ca2a268d6624553d2de4659ed6451d9ac60",
+                "shasum": ""
+            },
+            "require": {
+                "league/climate": "^3.1",
+                "nikic/php-parser": "^2.0|^3.0|^4.0"
+            },
+            "require-dev": {
+                "phpspec/prophecy": "~1.0",
+                "phpunit/phpunit": "^5.6"
+            },
+            "bin": [
+                "bin/phpa"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PhpAssumptions\\": "src/PhpAssumptions"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Rick Kuipers",
+                    "email": "io@rskuipers.com"
+                }
+            ],
+            "description": "Static code analysis tool to detect weak assumptions",
+            "support": {
+                "issues": "https://github.com/rskuipers/php-assumptions/issues",
+                "source": "https://github.com/rskuipers/php-assumptions/tree/0.8.0"
+            },
+            "time": "2019-04-04T07:11:49+00:00"
+        },
+        {
+            "name": "sebastian/code-unit-reverse-lookup",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Looks up which function or method a line of code belongs to",
+            "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:15:22+00:00"
+        },
+        {
+            "name": "sebastian/comparator",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git",
+                "reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
+                "reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "sebastian/diff": "^3.0",
+                "sebastian/exporter": "^3.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@2bepublished.at"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values for equality",
+            "homepage": "https://github.com/sebastianbergmann/comparator",
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/comparator/issues",
+                "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:04:30+00:00"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git",
+                "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
+                "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "symfony/process": "^2 || ^3.3 || ^4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff",
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues",
+                "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:59:04+00:00"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "4.2.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git",
+                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5"
+            },
+            "suggest": {
+                "ext-posix": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.2-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP environments",
+            "homepage": "http://www.github.com/sebastianbergmann/environment",
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/environment/issues",
+                "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:53:42+00:00"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "3.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git",
+                "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e",
+                "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "ext-mbstring": "*",
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables for visualization",
+            "homepage": "http://www.github.com/sebastianbergmann/exporter",
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/exporter/issues",
+                "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:47:53+00:00"
+        },
+        {
+            "name": "sebastian/finder-facade",
+            "version": "1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/finder-facade.git",
+                "reference": "167c45d131f7fc3d159f56f191a0a22228765e16"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16",
+                "reference": "167c45d131f7fc3d159f56f191a0a22228765e16",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1",
+                "symfony/finder": "^2.3|^3.0|^4.0|^5.0",
+                "theseer/fdomdocument": "^1.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": []
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
+            "homepage": "https://github.com/sebastianbergmann/finder-facade",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/finder-facade/issues",
+                "source": "https://github.com/sebastianbergmann/finder-facade/tree/1.2"
+            },
+            "abandoned": true,
+            "time": "2020-01-16T08:08:45+00:00"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git",
+                "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b",
+                "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "ext-dom": "*",
+                "phpunit/phpunit": "^8.0"
+            },
+            "suggest": {
+                "ext-uopz": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "keywords": [
+                "global state"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/global-state/issues",
+                "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:43:24+00:00"
+        },
+        {
+            "name": "sebastian/object-enumerator",
+            "version": "3.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+            "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:40:27+00:00"
+        },
+        {
+            "name": "sebastian/object-reflector",
+            "version": "1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-reflector.git",
+                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Allows reflection of object attributes, including inherited and non-public ones",
+            "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+                "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:37:18+00:00"
+        },
+        {
+            "name": "sebastian/phpcpd",
+            "version": "4.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpcpd.git",
+                "reference": "0d9afa762f2400de077b2192f4a9d127de0bb78e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/0d9afa762f2400de077b2192f4a9d127de0bb78e",
+                "reference": "0d9afa762f2400de077b2192f4a9d127de0bb78e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "php": "^7.1",
+                "phpunit/php-timer": "^2.0",
+                "sebastian/finder-facade": "^1.1",
+                "sebastian/version": "^1.0|^2.0",
+                "symfony/console": "^2.7|^3.0|^4.0"
+            },
+            "bin": [
+                "phpcpd"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Copy/Paste Detector (CPD) for PHP code.",
+            "homepage": "https://github.com/sebastianbergmann/phpcpd",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/phpcpd/issues",
+                "source": "https://github.com/sebastianbergmann/phpcpd/tree/4.1.0"
+            },
+            "time": "2018-09-17T17:17:27+00:00"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/recursion-context.git",
+                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
+                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP variables",
+            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:34:24+00:00"
+        },
+        {
+            "name": "sebastian/resource-operations",
+            "version": "2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/resource-operations.git",
+                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides a list of PHP built-in functions that operate on resources",
+            "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+                "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:30:19+00:00"
+        },
+        {
+            "name": "sebastian/type",
+            "version": "1.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/type.git",
+                "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4",
+                "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the types of the PHP type system",
+            "homepage": "https://github.com/sebastianbergmann/type",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/type/issues",
+                "source": "https://github.com/sebastianbergmann/type/tree/1.1.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:25:11+00:00"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git",
+                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
+                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/version/issues",
+                "source": "https://github.com/sebastianbergmann/version/tree/master"
+            },
+            "time": "2016-10-03T07:35:21+00:00"
+        },
+        {
+            "name": "seld/cli-prompt",
+            "version": "1.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/cli-prompt.git",
+                "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5",
+                "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12.63"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Seld\\CliPrompt\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be"
+                }
+            ],
+            "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type",
+            "keywords": [
+                "cli",
+                "console",
+                "hidden",
+                "input",
+                "prompt"
+            ],
+            "support": {
+                "issues": "https://github.com/Seldaek/cli-prompt/issues",
+                "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4"
+            },
+            "time": "2020-12-15T21:32:01+00:00"
+        },
+        {
+            "name": "symfony/config",
+            "version": "v4.4.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/config.git",
+                "reference": "1cb26cdb8a9834d8494cadd284602fa0647b73e5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/config/zipball/1cb26cdb8a9834d8494cadd284602fa0647b73e5",
+                "reference": "1cb26cdb8a9834d8494cadd284602fa0647b73e5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/filesystem": "^3.4|^4.0|^5.0",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-php81": "^1.22"
+            },
+            "conflict": {
+                "symfony/finder": "<3.4"
+            },
+            "require-dev": {
+                "symfony/event-dispatcher": "^3.4|^4.0|^5.0",
+                "symfony/finder": "^3.4|^4.0|^5.0",
+                "symfony/messenger": "^4.1|^5.0",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/yaml": "To use the yaml reference dumper"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Config\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/config/tree/v4.4.26"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-06-21T14:51:25+00:00"
+        },
+        {
+            "name": "symfony/console",
+            "version": "v4.4.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/console.git",
+                "reference": "9aa1eb46c1b12fada74dc0c529e93d1ccef22576"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/console/zipball/9aa1eb46c1b12fada74dc0c529e93d1ccef22576",
+                "reference": "9aa1eb46c1b12fada74dc0c529e93d1ccef22576",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/polyfill-php80": "^1.15",
+                "symfony/service-contracts": "^1.1|^2"
+            },
+            "conflict": {
+                "symfony/dependency-injection": "<3.4",
+                "symfony/event-dispatcher": "<4.3|>=5",
+                "symfony/lock": "<4.4",
+                "symfony/process": "<3.3"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0"
+            },
+            "require-dev": {
+                "psr/log": "~1.0",
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/lock": "^4.4|^5.0",
+                "symfony/process": "^3.4|^4.0|^5.0",
+                "symfony/var-dumper": "^4.3|^5.0"
+            },
+            "suggest": {
+                "psr/log": "For using the console logger",
+                "symfony/event-dispatcher": "",
+                "symfony/lock": "",
+                "symfony/process": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Console\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Eases the creation of beautiful and testable command line interfaces",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/console/tree/v4.4.26"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-06-06T09:12:27+00:00"
+        },
+        {
+            "name": "symfony/dependency-injection",
+            "version": "v4.4.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/dependency-injection.git",
+                "reference": "a944d2f8e903dc99f5f1baf3eb74081352f0067f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a944d2f8e903dc99f5f1baf3eb74081352f0067f",
+                "reference": "a944d2f8e903dc99f5f1baf3eb74081352f0067f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "psr/container": "^1.0",
+                "symfony/service-contracts": "^1.1.6|^2"
+            },
+            "conflict": {
+                "symfony/config": "<4.3|>=5.0",
+                "symfony/finder": "<3.4",
+                "symfony/proxy-manager-bridge": "<3.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "psr/container-implementation": "1.0",
+                "symfony/service-implementation": "1.0|2.0"
+            },
+            "require-dev": {
+                "symfony/config": "^4.3",
+                "symfony/expression-language": "^3.4|^4.0|^5.0",
+                "symfony/yaml": "^4.4|^5.0"
+            },
+            "suggest": {
+                "symfony/config": "",
+                "symfony/expression-language": "For using expressions in service container configuration",
+                "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required",
+                "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them",
+                "symfony/yaml": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\DependencyInjection\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/dependency-injection/tree/v4.4.26"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-06-24T08:08:16+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher",
+            "version": "v4.4.25",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher.git",
+                "reference": "047773e7016e4fd45102cedf4bd2558ae0d0c32f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/047773e7016e4fd45102cedf4bd2558ae0d0c32f",
+                "reference": "047773e7016e4fd45102cedf4bd2558ae0d0c32f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/event-dispatcher-contracts": "^1.1"
+            },
+            "conflict": {
+                "symfony/dependency-injection": "<3.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "1.1"
+            },
+            "require-dev": {
+                "psr/log": "~1.0",
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/error-handler": "~3.4|~4.4",
+                "symfony/expression-language": "^3.4|^4.0|^5.0",
+                "symfony/http-foundation": "^3.4|^4.0|^5.0",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/stopwatch": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/dependency-injection": "",
+                "symfony/http-kernel": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\EventDispatcher\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.25"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-26T17:39:37+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v1.1.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7",
+                "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3"
+            },
+            "suggest": {
+                "psr/event-dispatcher": "",
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-07-06T13:19:58+00:00"
+        },
+        {
+            "name": "symfony/filesystem",
+            "version": "v4.4.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/filesystem.git",
+                "reference": "a501126eb6ec9288a3434af01b3d4499ec1268a0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/a501126eb6ec9288a3434af01b3d4499ec1268a0",
+                "reference": "a501126eb6ec9288a3434af01b3d4499ec1268a0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Filesystem\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides basic utilities for the filesystem",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/filesystem/tree/v4.4.26"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-06-30T07:12:23+00:00"
+        },
+        {
+            "name": "symfony/finder",
+            "version": "v4.4.25",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/finder.git",
+                "reference": "ed33314396d968a8936c95f5bd1b88bd3b3e94a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/ed33314396d968a8936c95f5bd1b88bd3b3e94a3",
+                "reference": "ed33314396d968a8936c95f5bd1b88bd3b3e94a3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Finder\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Finds files and directories via an intuitive fluent interface",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/finder/tree/v4.4.25"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-26T11:20:16+00:00"
+        },
+        {
+            "name": "symfony/options-resolver",
+            "version": "v4.4.25",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/options-resolver.git",
+                "reference": "2e607d627c70aa56284a02d34fc60dfe3a9a352e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2e607d627c70aa56284a02d34fc60dfe3a9a352e",
+                "reference": "2e607d627c70aa56284a02d34fc60dfe3a9a352e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\OptionsResolver\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides an improved replacement for the array_replace PHP function",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "config",
+                "configuration",
+                "options"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/options-resolver/tree/v4.4.25"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-26T11:20:16+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
+                "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-02-19T12:13:01+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-idn",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65",
+                "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "symfony/polyfill-intl-normalizer": "^1.10",
+                "symfony/polyfill-php72": "^1.10"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Laurent Bassin",
+                    "email": "laurent@bassin.info"
+                },
+                {
+                    "name": "Trevor Rowbotham",
+                    "email": "trevor.rowbotham@pm.me"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "idn",
+                "intl",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-27T09:27:20+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-normalizer",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+                "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
+                "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's Normalizer class and related functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "intl",
+                "normalizer",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-02-19T12:13:01+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
+                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-27T09:17:38+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010",
+                "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-02-19T12:13:01+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/eca0bf41ed421bed1b57c4958bab16aa86b757d0",
+                "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-02-19T12:13:01+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php81",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php81.git",
+                "reference": "e66119f3de95efc359483f810c4c3e6436279436"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436",
+                "reference": "e66119f3de95efc359483f810c4c3e6436279436",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php81\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-21T13:25:03+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v4.4.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "7e812c84c3f2dba173d311de6e510edf701685a8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/7e812c84c3f2dba173d311de6e510edf701685a8",
+                "reference": "7e812c84c3f2dba173d311de6e510edf701685a8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Executes commands in sub-processes",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/process/tree/v4.4.26"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-06-09T14:57:04+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v1.1.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b776d18b303a39f56c63747bcb977ad4b27aca26",
+                "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/service-contracts/tree/v1.1.9"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-07-06T13:19:58+00:00"
+        },
+        {
+            "name": "symfony/stopwatch",
+            "version": "v4.4.25",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/stopwatch.git",
+                "reference": "80d9ae0c8a02bd291abf372764c0fc68cbd06c42"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/stopwatch/zipball/80d9ae0c8a02bd291abf372764c0fc68cbd06c42",
+                "reference": "80d9ae0c8a02bd291abf372764c0fc68cbd06c42",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/service-contracts": "^1.0|^2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Stopwatch\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides a way to profile code",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/stopwatch/tree/v4.4.25"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-26T17:39:37+00:00"
+        },
+        {
+            "name": "symfony/yaml",
+            "version": "v4.4.26",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/yaml.git",
+                "reference": "e096ef4b4c4c9a2f72c2ac660f54352cd31c60f8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/e096ef4b4c4c9a2f72c2ac660f54352cd31c60f8",
+                "reference": "e096ef4b4c4c9a2f72c2ac660f54352cd31c60f8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
+            },
+            "require-dev": {
+                "symfony/console": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/console": "For validating YAML files using the lint command"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Yaml\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Loads and dumps YAML files",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/yaml/tree/v4.4.26"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-06-23T19:06:53+00:00"
+        },
+        {
+            "name": "theseer/fdomdocument",
+            "version": "1.6.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/fDOMDocument.git",
+                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "lib-libxml": "*",
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
+            "homepage": "https://github.com/theseer/fDOMDocument",
+            "support": {
+                "issues": "https://github.com/theseer/fDOMDocument/issues",
+                "source": "https://github.com/theseer/fDOMDocument/tree/master"
+            },
+            "time": "2017-06-30T11:53:12+00:00"
+        },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git",
+                "reference": "75a63c33a8577608444246075ea0af0d052e452a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
+                "reference": "75a63c33a8577608444246075ea0af0d052e452a",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+            "support": {
+                "issues": "https://github.com/theseer/tokenizer/issues",
+                "source": "https://github.com/theseer/tokenizer/tree/master"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-07-12T23:59:07+00:00"
+        },
+        {
+            "name": "vimeo/psalm",
+            "version": "4.8.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/vimeo/psalm.git",
+                "reference": "f73f2299dbc59a3e6c4d66cff4605176e728ee69"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/vimeo/psalm/zipball/f73f2299dbc59a3e6c4d66cff4605176e728ee69",
+                "reference": "f73f2299dbc59a3e6c4d66cff4605176e728ee69",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2.4.2",
+                "amphp/byte-stream": "^1.5",
+                "composer/package-versions-deprecated": "^1.8.0",
+                "composer/semver": "^1.4 || ^2.0 || ^3.0",
+                "composer/xdebug-handler": "^1.1 || ^2.0",
+                "dnoegel/php-xdg-base-dir": "^0.1.1",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-simplexml": "*",
+                "ext-tokenizer": "*",
+                "felixfbecker/advanced-json-rpc": "^3.0.3",
+                "felixfbecker/language-server-protocol": "^1.5",
+                "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
+                "nikic/php-parser": "^4.10.5",
+                "openlss/lib-array2xml": "^1.0",
+                "php": "^7.1|^8",
+                "sebastian/diff": "^3.0 || ^4.0",
+                "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0",
+                "webmozart/path-util": "^2.3"
+            },
+            "provide": {
+                "psalm/psalm": "self.version"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.2",
+                "brianium/paratest": "^4.0||^6.0",
+                "ext-curl": "*",
+                "php-parallel-lint/php-parallel-lint": "^1.2",
+                "phpdocumentor/reflection-docblock": "^5",
+                "phpmyadmin/sql-parser": "5.1.0||dev-master",
+                "phpspec/prophecy": ">=1.9.0",
+                "phpunit/phpunit": "^9.0",
+                "psalm/plugin-phpunit": "^0.16",
+                "slevomat/coding-standard": "^7.0",
+                "squizlabs/php_codesniffer": "^3.5",
+                "symfony/process": "^4.3 || ^5.0",
+                "weirdan/phpunit-appveyor-reporter": "^1.0.0",
+                "weirdan/prophecy-shim": "^1.0 || ^2.0"
+            },
+            "suggest": {
+                "ext-igbinary": "^2.0.5"
+            },
+            "bin": [
+                "psalm",
+                "psalm-language-server",
+                "psalm-plugin",
+                "psalm-refactor",
+                "psalter"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.x-dev",
+                    "dev-3.x": "3.x-dev",
+                    "dev-2.x": "2.x-dev",
+                    "dev-1.x": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psalm\\": "src/Psalm/"
+                },
+                "files": [
+                    "src/functions.php",
+                    "src/spl_object_id.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Matthew Brown"
+                }
+            ],
+            "description": "A static analysis tool for finding errors in PHP applications",
+            "keywords": [
+                "code",
+                "inspection",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/vimeo/psalm/issues",
+                "source": "https://github.com/vimeo/psalm/tree/4.8.1"
+            },
+            "time": "2021-06-20T23:03:20+00:00"
+        },
+        {
+            "name": "webmozart/assert",
+            "version": "1.10.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/webmozarts/assert.git",
+                "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
+                "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "symfony/polyfill-ctype": "^1.8"
+            },
+            "conflict": {
+                "phpstan/phpstan": "<0.12.20",
+                "vimeo/psalm": "<4.6.1 || 4.6.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5.13"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.10-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Webmozart\\Assert\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                }
+            ],
+            "description": "Assertions to validate method input/output with nice error messages.",
+            "keywords": [
+                "assert",
+                "check",
+                "validate"
+            ],
+            "support": {
+                "issues": "https://github.com/webmozarts/assert/issues",
+                "source": "https://github.com/webmozarts/assert/tree/1.10.0"
+            },
+            "time": "2021-03-09T10:59:23+00:00"
+        },
+        {
+            "name": "webmozart/path-util",
+            "version": "2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/webmozart/path-util.git",
+                "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
+                "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "webmozart/assert": "~1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.6",
+                "sebastian/version": "^1.0.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Webmozart\\PathUtil\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                }
+            ],
+            "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.",
+            "support": {
+                "issues": "https://github.com/webmozart/path-util/issues",
+                "source": "https://github.com/webmozart/path-util/tree/2.3.0"
+            },
+            "time": "2015-12-17T08:42:14+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {
+        "roave/security-advisories": 20
+    },
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": "^7.2"
+    },
+    "platform-dev": [],
+    "platform-overrides": {
+        "php": "7.2"
+    },
+    "plugin-api-version": "2.0.0"
+}
diff --git a/infection.json b/infection.json
deleted file mode 100644
index db0b265..0000000
--- a/infection.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-    "source": {
-        "directories": [
-            "src"
-        ]
-    },
-    "logs": {
-        "text": "infection.log"
-    },
-    "mutators": {
-        "@default": true,
-        "global-ignoreSourceCodeByRegex": [
-            "throw new RuntimeException\\(sprintf\\('The assertion of %s::%s at line %d failed\\.'.+",
-            "for \\(.+\\) \\{"
-        ],
-        "DecrementInteger": {
-            "ignore": [
-                "MacFJA\\RediSearch\\Aggregate\\Exception\\UnknownSortDirectionException",
-                "MacFJA\\RediSearch\\Search\\Exception\\UnknownUnitException"
-            ]
-        },
-        "OneZeroInteger": {
-            "ignore": [
-                "MacFJA\\RediSearch\\Aggregate\\Exception\\UnknownSortDirectionException",
-                "MacFJA\\RediSearch\\Search\\Exception\\UnknownUnitException"
-            ]
-        }
-    }
-}
\ No newline at end of file
diff --git a/phpmd.xml b/phpmd.xml
deleted file mode 100644
index ace9289..0000000
--- a/phpmd.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ruleset name="pcsg-generated-ruleset"
-         xmlns="http://pmd.sf.net/ruleset/1.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
-         xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
-    <description>Created with the PHP Coding Standard Generator. http://edorian.github.com/php-coding-standard-generator/
-    </description>
-    <rule ref="rulesets/codesize.xml"/>
-    <rule ref="rulesets/controversial.xml"/>
-    <rule ref="rulesets/design.xml"/>
-    <rule ref="rulesets/naming.xml"/>
-    <rule ref="rulesets/unusedcode.xml"/>
-</ruleset>
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
index 48f4986..6900a90 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,6 +1,12 @@
 parameters:
-	ignoreErrors:
-		- '#type has no value type specified in iterable type Predis\\Client#'
-		- '#with no value type specified in iterable type Predis\\Client#'
-		- message: '#MacFJA\\RediSearch\\Index\\Builder\\\w+ constructor expects [\w\|]+, bool\|float\|int\|string\|null given#'
-		  path: src/Index/InfoResult.php
\ No newline at end of file
+    level: 8
+    paths:
+        - src
+        - tests
+    ignoreErrors:
+        - '#Method MacFJA\\RediSearch\\Redis\\Command\\\w+::getLanguageOptions\(\) should return#'
+        - '#uses generic trait MacFJA\\RediSearch\\Redis\\Command\\Option\\DecoratedOptionTrait but does not specify its types: T#'
+        - '#Call to an undefined method MacFJA\\RediSearch\\Redis\\Command\\Option\\CustomValidatorOption::setValue\(\).#'
+        -
+            message: '#Call to an undefined method MacFJA\\RediSearch\\Redis\\Command\\Option\\CommandOption::set\w+\(\).#'
+            path: src/Redis/Command/Option/GroupedOption.php
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
deleted file mode 100644
index 6fe1f1c..0000000
--- a/phpunit.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
-         bootstrap="vendor/autoload.php"
-         cacheResultFile=".phpunit.cache/test-results"
-         executionOrder="depends,defects"
-         forceCoversAnnotation="true"
-         beStrictAboutCoversAnnotation="true"
-         beStrictAboutOutputDuringTests="true"
-         beStrictAboutTodoAnnotatedTests="true"
-         stopOnFailure="true"
-         stopOnError="true"
-         stopOnDefect="true"
-         failOnEmptyTestSuite="true"
-         stopOnWarning="true"
-         beStrictAboutTestsThatDoNotTestAnything="true"
-         beStrictAboutResourceUsageDuringSmallTests="true"
-         beStrictAboutChangesToGlobalState="true"
-         cacheResult="true"
-         convertDeprecationsToExceptions="true"
-         convertErrorsToExceptions="true"
-         convertNoticesToExceptions="true"
-         convertWarningsToExceptions="true"
-         failOnIncomplete="false"
-         failOnSkipped="false"
-         stopOnIncomplete="false"
-         stopOnRisky="false"
-         stopOnSkipped="false"
-         failOnRisky="true"
-         failOnWarning="true"
-         verbose="true">
-    <testsuites>
-        <testsuite name="default">
-            <directory suffix="Test.php">tests</directory>
-        </testsuite>
-    </testsuites>
-
-    <coverage cacheDirectory=".phpunit.cache/code-coverage"
-              processUncoveredFiles="true">
-        <include>
-            <directory suffix=".php">src</directory>
-        </include>
-    </coverage>
-</phpunit>
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..e1e87b0
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         executionOrder="depends,defects"
+         forceCoversAnnotation="true"
+         beStrictAboutCoversAnnotation="true"
+         beStrictAboutOutputDuringTests="true"
+         beStrictAboutTodoAnnotatedTests="true"
+         verbose="true">
+    <testsuites>
+        <testsuite name="default">
+            <directory suffix="Test.php">tests</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <whitelist addUncoveredFilesFromWhitelist="true" processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">src</directory>
+        </whitelist>
+    </filter>
+</phpunit>
diff --git a/psalm.xml b/psalm.xml
index 29dffb1..7c0333d 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -1,11 +1,10 @@
 <?xml version="1.0"?>
 <psalm
-        totallyTyped="false"
-        errorLevel="5"
-        resolveFromConfigFile="true"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xmlns="https://getpsalm.org/schema/config"
-        xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+    errorLevel="7"
+    resolveFromConfigFile="true"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns="https://getpsalm.org/schema/config"
+    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
 >
     <projectFiles>
         <directory name="src" />
@@ -13,11 +12,4 @@
             <directory name="vendor" />
         </ignoreFiles>
     </projectFiles>
-
-<!--    <issueHandlers>-->
-<!--        <PropertyNotSetInConstructor/>-->
-<!--        <MissingParamType/>-->
-<!--        <PossiblyNullArgument/>-->
-<!--        <ImplicitToStringCast errorLevel="suppress" />-->
-<!--    </issueHandlers>-->
-</psalm>
\ No newline at end of file
+</psalm>
diff --git a/src/Aggregate.php b/src/Aggregate.php
deleted file mode 100644
index b49c75e..0000000
--- a/src/Aggregate.php
+++ /dev/null
@@ -1,282 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch;
-
-use function array_keys;
-use function array_map;
-use function array_merge;
-use function array_shift;
-use function assert;
-use function is_int;
-use function is_string;
-use MacFJA\RediSearch\Aggregate\GroupBy;
-use MacFJA\RediSearch\Aggregate\Reducer;
-use MacFJA\RediSearch\Aggregate\Result;
-use MacFJA\RediSearch\Aggregate\SortBy;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\PaginatedResult;
-use MacFJA\RediSearch\Helper\PipelineItem;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use Predis\Client;
-use Throwable;
-
-class Aggregate implements Builder, Pipeable
-{
-    /** @var Client */
-    private $redis;
-
-    /** @var null|string */
-    private $indexName;
-
-    /** @var null|string */
-    private $query;
-
-    /** @var bool */
-    private $verbatim = false;
-
-    /** @var array<string> */
-    private $load = [];
-
-    /** @var array<GroupBy> */
-    private $groupBy = [];
-
-    /** @var null|SortBy */
-    private $sortBy;
-
-    /** @var array<string,string> */
-    private $applies = [];
-
-    /** @var null|int */
-    private $resultOffset;
-
-    /** @var null|int */
-    private $resultLimit;
-
-    /** @var null|string */
-    private $filter;
-
-    public function __construct(Client $redis)
-    {
-        $this->redis = $redis;
-    }
-
-    public function reset(): Builder
-    {
-        $this->indexName = null;
-        $this->query = null;
-        $this->verbatim = false;
-        $this->load = [];
-        $this->groupBy = [];
-        $this->sortBy = null;
-        $this->applies = [];
-        $this->resultOffset = null;
-        $this->resultLimit = null;
-        $this->filter = null;
-
-        return $this;
-    }
-
-    public function withIndexName(string $indexName): Aggregate
-    {
-        $this->indexName = $indexName;
-
-        return $this;
-    }
-
-    public function withQuery(string $query): Aggregate
-    {
-        $this->query = $query;
-
-        return $this;
-    }
-
-    public function withVerbatim(bool $verbatim = true): Aggregate
-    {
-        $this->verbatim = $verbatim;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $load
-     *
-     * @throws Throwable
-     *
-     * @return $this
-     */
-    public function withLoad(array $load): Aggregate
-    {
-        DataHelper::assertArrayOf($load, 'string');
-        $this->load = $load;
-
-        return $this;
-    }
-
-    /**
-     * @param array<GroupBy> $groupBy
-     *
-     * @throws Throwable
-     *
-     * @return $this
-     */
-    public function withGroupBy(array $groupBy): Aggregate
-    {
-        DataHelper::assertArrayOf($groupBy, GroupBy::class);
-        $this->groupBy = $groupBy;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string>  $properties
-     * @param array<Reducer> $reducers
-     *
-     * @throws Throwable
-     *
-     * @return $this
-     */
-    public function addGroupBy(array $properties, array $reducers): self
-    {
-        $this->groupBy[] = new GroupBy($properties, $reducers);
-
-        return $this;
-    }
-
-    /**
-     * @param array<string,string> $properties
-     * @psalm-param array<string,"ASC"|"DESC"> $properties
-     * @phpstan-param array<string,"ASC"|"DESC"> $properties
-     *
-     * @throws Throwable
-     *
-     * @return $this
-     */
-    public function withSortBy(array $properties, ?int $max): Aggregate
-    {
-        $this->sortBy = new SortBy($properties, $max);
-
-        return $this;
-    }
-
-    /**
-     * @param array<string,string> $applies
-     *
-     * @throws Throwable
-     *
-     * @return $this
-     */
-    public function withApplies(array $applies): Aggregate
-    {
-        DataHelper::assertArrayOf(array_keys($applies), 'string');
-        DataHelper::assertArrayOf($applies, 'string');
-        $this->applies = $applies;
-
-        return $this;
-    }
-
-    public function withResultOffset(int $resultOffset): Aggregate
-    {
-        $this->resultOffset = $resultOffset;
-
-        return $this;
-    }
-
-    public function withResultLimit(int $resultLimit): Aggregate
-    {
-        $this->resultLimit = $resultLimit;
-
-        return $this;
-    }
-
-    public function withFilter(string $filter): Aggregate
-    {
-        $this->filter = $filter;
-
-        return $this;
-    }
-
-    /**
-     * @return array<Result>
-     *
-     * @deprecated Use \MacFJA\RediSearch\Aggregate::execute
-     */
-    public function aggregate(): array
-    {
-        return $this->execute()->getItems();
-    }
-
-    /**
-     * @return PaginatedResult<Result>
-     */
-    public function execute()
-    {
-        $request = $this->asPipelineItem();
-        $rawResult = $this->redis->executeCommand($request->getCommand());
-
-        return $request->transform($rawResult);
-    }
-
-    public function asPipelineItem(): PipelineItem
-    {
-        try {
-            return PipelineItem::createFromRaw(
-                $this->buildQuery(),
-                static function ($rawResult, array $context): PaginatedResult {
-                    $totalCount = array_shift($rawResult);
-                    assert(is_int($totalCount));
-
-                    $items = array_map(function (array $document) {
-                        return new Result(RedisHelper::getPairs($document));
-                    }, $rawResult);
-
-                    return new class($totalCount, $items, $context['offset'] ?? 0, $context['limit'] ?? 0) extends PaginatedResult {
-                    };
-                },
-                ['offset' => $this->resultOffset, 'limit' => $this->resultLimit]
-            );
-        } finally {
-            $this->reset();
-        }
-    }
-
-    /**
-     * @return array<float|int|string>
-     */
-    private function buildQuery(): array
-    {
-        assert(is_string($this->indexName));
-        assert(is_string($this->query));
-        $query = ['FT.AGGREGATE', $this->indexName, $this->query];
-        $query = RedisHelper::buildQueryBoolean($query, ['VERBATIM' => $this->verbatim]);
-        $query = RedisHelper::buildQueryList($query, ['LOAD' => $this->load]);
-        $query = RedisHelper::buildQueryNotNull($query, ['FILTER' => $this->filter]);
-        $query = RedisHelper::buildQueryPartial($query, array_merge([$this->sortBy], $this->groupBy));
-        foreach ($this->applies as $expression => $alias) {
-            $query = array_merge($query, ['APPLY', $expression, 'AS', $alias]);
-        }
-        if (is_int($this->resultOffset) && is_int($this->resultLimit)) {
-            $query = array_merge($query, ['LIMIT', $this->resultOffset, $this->resultLimit]);
-        }
-
-        return $query;
-    }
-}
diff --git a/src/Aggregate/SortBy.php b/src/Aggregate/SortBy.php
deleted file mode 100644
index e9d4b96..0000000
--- a/src/Aggregate/SortBy.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Aggregate;
-
-use function array_diff;
-use function array_keys;
-use function array_merge;
-use function array_unique;
-use function count;
-use MacFJA\RediSearch\Aggregate\Exception\UnknownSortDirectionException;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use OutOfRangeException;
-use function strpos;
-use Throwable;
-
-class SortBy implements \MacFJA\RediSearch\PartialQuery
-{
-    public const SORT_ASC = 'ASC';
-
-    public const SORT_DESC = 'DESC';
-
-    private const SORT_ACCEPTABLE_VALUES = [self::SORT_ASC, self::SORT_DESC];
-
-    /**
-     * @psalm-var array<string,"ASC"|"DESC">
-     * @phpstan-var array<string,"ASC"|"DESC">
-     *
-     * @var array<string,string>
-     */
-    private $properties = [];
-
-    /** @var null|int */
-    private $max;
-
-    /**
-     * @psalm-param array<string,"ASC"|"DESC"> $properties
-     * @phpstab-param array<string,"ASC"|"DESC"> $properties
-     *
-     * @param array<string,string> $properties
-     *
-     * @throws Throwable
-     */
-    public function __construct(array $properties, ?int $max = null)
-    {
-        DataHelper::assertArrayOf(array_keys($properties), 'string');
-        $allValues = array_unique(array_merge($properties, self::SORT_ACCEPTABLE_VALUES));
-        DataHelper::assert(
-            2 === count($allValues),
-            new UnknownSortDirectionException(array_diff($allValues, self::SORT_ACCEPTABLE_VALUES))
-
-        );
-        DataHelper::assert(null === $max || $max >= 0, new OutOfRangeException('MAX such by greater or equals to 0'));
-        $this->properties = $properties;
-        $this->max = $max;
-    }
-
-    public function getQueryParts(): array
-    {
-        $query = ['SORTBY', count($this->properties) * 2];
-        foreach ($this->properties as $property => $direction) {
-            if (!(0 === strpos($property, '@'))) {
-                $property = '@'.$property;
-            }
-            $query[] = $property;
-            $query[] = $direction;
-        }
-
-        return RedisHelper::buildQueryNotNull($query, ['MAX' => $this->max]);
-    }
-}
diff --git a/src/Aggregate/Exception/NotEnoughReducersException.php b/src/Exception/NotEnoughFieldsException.php
similarity index 83%
rename from src/Aggregate/Exception/NotEnoughReducersException.php
rename to src/Exception/NotEnoughFieldsException.php
index 67dcfb0..53b45bd 100644
--- a/src/Aggregate/Exception/NotEnoughReducersException.php
+++ b/src/Exception/NotEnoughFieldsException.php
@@ -19,19 +19,14 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Aggregate\Exception;
+namespace MacFJA\RediSearch\Exception;
 
 use OutOfRangeException;
 use Throwable;
 
-/**
- * @codeCoverageIgnore
- *
- * @deprecated This exception is never used.
- */
-class NotEnoughReducersException extends OutOfRangeException
+class NotEnoughFieldsException extends OutOfRangeException
 {
-    public function __construct(string $message = 'You must have at least one reducer', int $code = 0, ?Throwable $previous = null)
+    public function __construct(string $message = 'You must have at least one field', int $code = 0, ?Throwable $previous = null)
     {
         parent::__construct($message, $code, $previous);
     }
diff --git a/src/Words/Exception/NotEnoughTermsException.php b/src/Exception/NotEnoughTermsException.php
similarity index 96%
rename from src/Words/Exception/NotEnoughTermsException.php
rename to src/Exception/NotEnoughTermsException.php
index 8e1b511..5806995 100644
--- a/src/Words/Exception/NotEnoughTermsException.php
+++ b/src/Exception/NotEnoughTermsException.php
@@ -19,7 +19,7 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Words\Exception;
+namespace MacFJA\RediSearch\Exception;
 
 use OutOfRangeException;
 use Throwable;
diff --git a/src/Search/Exception/OutOfRangeLevenshteinDistanceException.php b/src/Exception/OutOfRangeLevenshteinDistanceException.php
similarity index 97%
rename from src/Search/Exception/OutOfRangeLevenshteinDistanceException.php
rename to src/Exception/OutOfRangeLevenshteinDistanceException.php
index 15c7b22..10b0c36 100644
--- a/src/Search/Exception/OutOfRangeLevenshteinDistanceException.php
+++ b/src/Exception/OutOfRangeLevenshteinDistanceException.php
@@ -19,7 +19,7 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\Exception;
+namespace MacFJA\RediSearch\Exception;
 
 use OutOfRangeException;
 use function sprintf;
diff --git a/src/Search/Exception/UnknownUnitException.php b/src/Exception/UnknownUnitException.php
similarity index 96%
rename from src/Search/Exception/UnknownUnitException.php
rename to src/Exception/UnknownUnitException.php
index 5590c4c..e80403c 100644
--- a/src/Search/Exception/UnknownUnitException.php
+++ b/src/Exception/UnknownUnitException.php
@@ -19,7 +19,7 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\Exception;
+namespace MacFJA\RediSearch\Exception;
 
 use function sprintf;
 use Throwable;
diff --git a/src/Helper/DataHelper.php b/src/Helper/DataHelper.php
deleted file mode 100644
index bfae3e0..0000000
--- a/src/Helper/DataHelper.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Helper;
-
-use function assert;
-use function class_exists;
-use function debug_backtrace;
-use function end;
-use function gettype;
-use function in_array;
-use function is_array;
-use function is_scalar;
-use function is_string;
-use MacFJA\RediSearch\Index\Exception\IndexNotFoundException;
-use MacFJA\RediSearch\Search\Exception\SyntaxErrorException;
-use RuntimeException;
-use function settype;
-use function sprintf;
-use function sscanf;
-use function strpos;
-use function strtolower;
-use function substr;
-use Throwable;
-use UnexpectedValueException;
-
-class DataHelper
-{
-    /**
-     * @param null|string|Throwable $exception
-     *
-     * @throws Throwable
-     */
-    public static function assert(bool $condition, $exception = null, int $deep = 1): void
-    {
-        if (false === $condition) {
-            if ($exception instanceof Throwable) {
-                throw $exception;
-            }
-            if (is_string($exception) && class_exists($exception)) {
-                $instance = new $exception();
-                if ($instance instanceof Throwable) {
-                    throw $instance;
-                }
-            }
-            $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1 + $deep);
-            $trace = end($trace);
-            assert(is_array($trace));
-
-            throw new RuntimeException(sprintf(
-                'The assertion of %s::%s at line %d failed.',
-                $trace['class'] ?? '',
-                $trace['function'] ?? '',
-                $trace['line'] ?? 0
-            ));
-        }
-    }
-
-    /**
-     * @param array<mixed>          $array
-     * @param null|string|Throwable $exception
-     *
-     * @throws Throwable
-     */
-    public static function assertArrayOf(array $array, string $type, $exception = null): void
-    {
-        $allowNull = false;
-        if (0 === strpos($type, '?')) {
-            $allowNull = true;
-            $type = substr($type, 1);
-            assert(is_string($type));
-        }
-        $type = self::getPhpType($type);
-        foreach ($array as $item) {
-            if (true === $allowNull && null === $item) {
-                continue;
-            }
-            if (in_array($type, ['string', 'boolean', 'integer', 'double'], true)) {
-                self::assert(gettype($item) === $type, $exception, 2);
-            } elseif ('scalar' === $type) {
-                self::assert(is_scalar($item), $exception, 2);
-            } else {
-                self::assert($item instanceof $type, $exception, 2);
-            }
-        }
-    }
-
-    /**
-     * @param array<mixed>|mixed $result
-     *
-     * @throws IndexNotFoundException
-     * @throws SyntaxErrorException
-     */
-    public static function handleRawResult($result): void
-    {
-        if (is_string($result) && !(false === strpos($result, ': no such index'))) {
-            sscanf($result, '%s: no such index', $index);
-
-            throw new IndexNotFoundException((string) $index);
-        }
-        if (is_string($result) && 'unknown index name' === strtolower($result)) {
-            throw new IndexNotFoundException();
-        }
-        if (is_string($result) && 0 === strpos($result, 'Syntax error')) {
-            throw new SyntaxErrorException($result);
-        }
-    }
-
-    /**
-     * @param null|bool|float|int|string $raw
-     *
-     * @return null|bool|float|int|string
-     */
-    public static function nullOrCast($raw, string $cast)
-    {
-        if (is_scalar($raw)) {
-            settype($raw, self::getPhpType($cast, true));
-        }
-
-        return $raw;
-    }
-
-    private static function getPhpType(string $type, bool $exceptionOnUnknown = false): string
-    {
-        switch ($type) {
-            case 'int':
-            case 'integer':
-                return 'integer';
-            case 'bool':
-            case 'boolean':
-                return 'boolean';
-            case 'float':
-            case 'double':
-                return 'double';
-            case 'string':
-                return 'string';
-        }
-        if (true === $exceptionOnUnknown) {
-            throw new UnexpectedValueException(sprintf('The type "%s" is unknown', $type));
-        }
-
-        return $type;
-    }
-}
diff --git a/src/Helper/PaginatedResult.php b/src/Helper/PaginatedResult.php
deleted file mode 100644
index 5cd2265..0000000
--- a/src/Helper/PaginatedResult.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Helper;
-
-use function ceil;
-use function count;
-
-/**
- * @template T
- */
-abstract class PaginatedResult
-{
-    /** @var int */
-    private $totalCount;
-
-    /** @var array<T> */
-    private $items;
-
-    /** @var int */
-    private $offset;
-
-    /** @var int */
-    private $pageSize;
-
-    /**
-     * @param array<T> $items
-     */
-    public function __construct(int $totalCount, array $items, int $offset, int $pageSize)
-    {
-        $this->totalCount = $totalCount;
-        $this->items = $items;
-        $this->offset = $offset;
-        $this->pageSize = $pageSize;
-    }
-
-    public function getTotalCount(): int
-    {
-        return $this->totalCount;
-    }
-
-    /**
-     * @return array<T>
-     */
-    public function getItems(): array
-    {
-        return $this->items;
-    }
-
-    public function getOffset(): int
-    {
-        return $this->offset;
-    }
-
-    public function getPageSize(): int
-    {
-        return $this->pageSize;
-    }
-
-    public function getPageNumber(): int
-    {
-        if (0 === $this->pageSize) {
-            return 0;
-        }
-
-        return (int) ceil($this->getOffset() / $this->pageSize) + 1;
-    }
-
-    public function havePreviousPage(): bool
-    {
-        return $this->offset > 0;
-    }
-
-    public function haveNextPage(): bool
-    {
-        return $this->offset + count($this->items) < $this->totalCount;
-    }
-
-    public function getPageCount(): int
-    {
-        if (0 === $this->pageSize) {
-            return 0;
-        }
-
-        return (int) ceil($this->totalCount / $this->pageSize);
-    }
-}
diff --git a/src/Helper/RedisHelper.php b/src/Helper/RedisHelper.php
deleted file mode 100644
index bbce26d..0000000
--- a/src/Helper/RedisHelper.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Helper;
-
-use function array_chunk;
-use function array_column;
-use function array_combine;
-use function array_filter;
-use function array_keys;
-use function array_merge;
-use function array_pop;
-use function array_shift;
-use function assert;
-use function count;
-use function is_array;
-use MacFJA\RediSearch\PartialQuery;
-
-class RedisHelper
-{
-    /**
-     * [
-     *   'REDISEARCHKEYWORD' => $phpBooleanValue
-     * ].
-     *
-     * @param array<float|int|string> $query
-     * @param array<string,bool>      $data
-     *
-     * @return array<float|int|string>
-     */
-    public static function buildQueryBoolean(array $query, array $data): array
-    {
-        return array_merge($query, array_keys(array_filter($data, function (bool $item): bool {
-            return $item;
-        })));
-    }
-
-    /**
-     * [
-     *   'REDISEARCHKEYWORD' => $phpArray
-     * ].
-     *
-     * @param array<float|int|string> $query
-     * @param array<string,array>     $data
-     *
-     * @return array<float|int|string>
-     */
-    public static function buildQueryList(array $query, array $data, bool $allowEmpty = false): array
-    {
-        foreach ($data as $queryWord => $list) {
-            if (!is_array($list)) {
-                continue;
-            }
-            if (true === $allowEmpty || count($list) > 0) {
-                $query[] = $queryWord;
-                $query[] = count($list);
-                $query = array_merge($query, $list);
-            }
-        }
-
-        return $query;
-    }
-
-    /**
-     * @param array<float|int|string>   $query
-     * @param array<string, null|mixed> $data
-     *
-     * @return array<float|int|string>
-     *
-     * @phan-suppress PhanPartialTypeMismatchReturn
-     */
-    public static function buildQueryNotNull($query, array $data): array
-    {
-        $notNullValues = array_filter($data, function ($value): bool {
-            return !(null === $value);
-        });
-        foreach ($notNullValues as $queryWord => $value) {
-            $query[] = $queryWord;
-            $query[] = $value;
-        }
-
-        return $query;
-    }
-
-    /**
-     * @param array<float|int|string>  $query
-     * @param array<null|PartialQuery> $partials
-     *
-     * @return array<float|int|string>
-     */
-    public static function buildQueryPartial(array $query, array $partials): array
-    {
-        /** @var PartialQuery $partial */
-        foreach (array_filter($partials) as $partial) {
-            if (!$partial instanceof PartialQuery) {
-                continue;
-            }
-            $query = array_merge($query, $partial->getQueryParts());
-        }
-
-        return $query;
-    }
-
-    /**
-     * @param array<float|int|mixed|string> $notKeyedArray
-     *
-     * @return array<string,mixed>
-     */
-    public static function getPairs(array $notKeyedArray): array
-    {
-        if (count($notKeyedArray) % 2 > 0) {
-            array_pop($notKeyedArray);
-        }
-        $entries = array_chunk($notKeyedArray, 2);
-        $keyed = array_combine(array_column($entries, 0), array_column($entries, 1));
-        assert(is_array($keyed));
-
-        return $keyed;
-    }
-
-    /**
-     * @param array<bool|float|int|string> $notKeyedArray
-     *
-     * @return null|bool|float|int|string
-     */
-    public static function getValue(array $notKeyedArray, string $key)
-    {
-        while (count($notKeyedArray) >= 2) {
-            $shifted = array_shift($notKeyedArray);
-            if ($shifted === $key) {
-                return array_shift($notKeyedArray);
-            }
-        }
-
-        return null;
-    }
-}
diff --git a/src/Index.php b/src/Index.php
index cb99d12..b819841 100644
--- a/src/Index.php
+++ b/src/Index.php
@@ -21,113 +21,110 @@
 
 namespace MacFJA\RediSearch;
 
-use function array_keys;
-use function array_map;
-use function array_merge;
+use function count;
 use function is_array;
 use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Index\Builder\Field;
-use MacFJA\RediSearch\Index\InfoResult;
-use Predis\Client;
-use Throwable;
-use function trigger_error;
-use function uniqid;
-
+use MacFJA\RediSearch\Redis\Command\AliasAdd;
+use MacFJA\RediSearch\Redis\Command\AliasDel;
+use MacFJA\RediSearch\Redis\Command\AliasUpdate;
+use MacFJA\RediSearch\Redis\Command\Alter;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
+use MacFJA\RediSearch\Redis\Command\DropIndex;
+use MacFJA\RediSearch\Redis\Command\Info;
+use MacFJA\RediSearch\Redis\Command\TagVals;
+use MacFJA\RediSearch\Redis\Initializer;
+use MacFJA\RediSearch\Redis\Response\InfoResponse;
+use Predis\ClientInterface;
+
+/**
+ * @codeCoverageIgnore
+ */
 class Index
 {
+    /** @var ClientInterface */
+    private $client;
+    /** @var InfoResponse */
+    private $info;
     /** @var string */
-    private $indexKey;
-
-    /** @var Client */
-    private $redis;
-
-    public function __construct(string $indexKey, Client $redis)
-    {
-        $this->indexKey = $indexKey;
-        $this->redis = $redis;
-    }
+    private $index;
+    /** @var string */
+    private $version;
 
-    /**
-     * @param array<string,float|int|string> $properties
-     *
-     * @throws Throwable
-     *
-     * @deprecated Use `addDocumentFromArray` instead
-     * @see Index::addDocumentFromArray()
-     */
-    public function addFromArray(array $properties, ?string $hash = null): string
+    public function __construct(string $index, ClientInterface $client)
     {
-        trigger_error('The method '.__METHOD__.' is deprecated. Use \MacFJA\RediSearch\Index::addDocumentFromArray instead.', \E_USER_DEPRECATED);
-
-        return $this->addDocumentFromArray($properties, $hash);
+        $this->client = $client;
+        $this->index = $index;
+        $this->version = Initializer::getRediSearchVersion($client) ?? '2.0.0';
+        $this->getInfo();
     }
 
     /**
      * @param array<string,float|int|string> $properties
-     *
-     * @throws Throwable
      */
     public function addDocumentFromArray(array $properties, ?string $hash = null): string
     {
-        DataHelper::assertArrayOf(array_keys($properties), 'string');
-        DataHelper::assertArrayOf($properties, 'scalar');
-        $documentId = is_string($hash) ? $hash : uniqid();
-        $query = ['HSET', $documentId];
+        $prefixes = $this->info->getIndexDefinition('prefixes');
+        $prefix = '';
+        if (is_array($prefixes) && count($prefixes) > 0) {
+            $prefix = (string) reset($prefixes);
+        }
+        $documentId = is_string($hash) ? $hash : uniqid($prefix, true);
+        $query = [$documentId];
         foreach ($properties as $name => $value) {
             $query[] = $name;
             $query[] = $value;
         }
 
-        $this->redis->executeRaw($query);
+        $this->client->hset(...$query);
 
         return $documentId;
     }
 
     public function deleteDocument(string $hash): bool
     {
-        $count = $this->redis->del($hash);
+        $count = $this->client->del($hash);
 
         return 1 === $count;
     }
 
-    public function addField(Field $field): bool
+    public function addField(CreateCommandFieldOption $field): bool
     {
-        $result = $this->redis->executeRaw(array_merge([
-            'FT.ALTER',
-            $this->indexKey,
-            'SCHEMA',
-            'ADD',
-        ], $field->getQueryParts()));
-
-        return 'OK' === $result;
+        $command = new Alter($this->version);
+        $command
+            ->setIndex($this->index)
+            ->addField($field)
+        ;
+
+        return 'OK' === (string) $this->client->executeCommand($command);
     }
 
     public function delete(bool $withDocuments = false): bool
     {
-        $query = ['FT.DROPINDEX', $this->indexKey];
-        if (true === $withDocuments) {
-            $query[] = 'DD';
-        }
-        $result = $this->redis->executeRaw($query);
+        $command = new DropIndex($this->version);
+        $command->setIndex($this->index)
+            ->setDeleteDocument($withDocuments)
+        ;
 
-        return 'OK' === $result;
+        return 'OK' === (string) $this->client->executeCommand($command);
     }
 
     public function addAlias(string $alias): bool
     {
-        return $this->redis->executeRaw(['FT.ALIASADD', $alias, $this->indexKey]);
+        return 'OK' === (string) $this->client->executeCommand(
+            (new AliasAdd($this->version))
+                ->setIndex($this->index)
+                ->setAlias($alias)
+        );
     }
 
     public function updateAlias(string $alias): bool
     {
-        return $this->redis->executeRaw(['FT.ALIASUPDATE', $alias, $this->indexKey]);
+        return 'OK' === (string) $this->client->executeCommand((new AliasUpdate($this->version))->setIndex($this->index)->setAlias($alias));
     }
 
     public function deleteAlias(string $alias): bool
     {
-        return $this->redis->executeRaw(['FT.ALIASDEL', $alias]);
+        return 'OK' === (string) $this->client->executeCommand((new AliasDel($this->version))->setAlias($alias));
     }
 
     /**
@@ -135,48 +132,13 @@ public function deleteAlias(string $alias): bool
      */
     public function getTagValues(string $fieldName): array
     {
-        return $this->redis->executeRaw(['FT.TAGVALS', $this->indexKey, $fieldName]);
+        return $this->client->executeCommand((new TagVals($this->version))->setIndex($this->index)->setField($fieldName));
     }
 
-    public function getStats(): InfoResult
+    public function getInfo(): InfoResponse
     {
-        $result = $this->redis->executeRaw(['FT.INFO', $this->indexKey]);
-        DataHelper::handleRawResult($result);
-
-        $stats = RedisHelper::getPairs($result);
-        $definitions = RedisHelper::getPairs($stats['index_definition'] ?? []);
-        $definitions = array_map(function ($item) {
-            if (!is_array($item)) {
-                return (string) $item;
-            }
-
-            return $item;
-        }, $definitions);
-        $definitions['prefixes'] = array_map('strval', (array) ($definitions['prefixes'] ?? []));
-
-        return new InfoResult(
-            (string) $stats['index_name'],
-            array_map('strval', (array) ($stats['index_options'] ?? [])),
-            $definitions,
-            (array) ($stats['fields'] ?? []),
-            (int) $stats['num_docs'],
-            (int) $stats['max_doc_id'],
-            (int) $stats['num_terms'],
-            (float) $stats['bytes_per_record_avg'],
-            (float) $stats['inverted_sz_mb'],
-            (int) ($stats['total_inverted_index_blocks'] ?? 0),
-            '1' === $stats['indexing'],
-            (int) ($stats['hash_indexing_failures'] ?? 0),
-            (float) ($stats['percent_indexed'] ?? 1),
-            array_map('strval', (array) ($stats['stopwords_list'] ?? []))
-        );
-    }
+        $this->info = $this->client->executeCommand((new Info($this->version))->setIndex($this->index));
 
-    /**
-     * @return array<string>
-     */
-    public static function getIndexes(Client $redis): array
-    {
-        return $redis->executeRaw(['FT._LIST']);
+        return $this->info;
     }
 }
diff --git a/src/Index/Builder.php b/src/Index/Builder.php
deleted file mode 100644
index e3e865e..0000000
--- a/src/Index/Builder.php
+++ /dev/null
@@ -1,396 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Index;
-
-use function array_filter;
-use function array_merge;
-use function assert;
-use function count;
-use function in_array;
-use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Index\Builder\Exception\MissingNameException;
-use MacFJA\RediSearch\Index\Builder\Exception\TooManyFields;
-use MacFJA\RediSearch\Index\Builder\Exception\UnsupportedLanguageException;
-use MacFJA\RediSearch\Index\Builder\Field;
-use MacFJA\RediSearch\Index\Builder\GeoField;
-use MacFJA\RediSearch\Index\Builder\NumericField;
-use MacFJA\RediSearch\Index\Builder\TagField;
-use MacFJA\RediSearch\Index\Builder\TextField;
-use Predis\Client;
-use Throwable;
-
-/**
- * @SuppressWarnings(PHPMD.TooManyFields) -- Builder Class
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- Builder Class
- */
-class Builder implements \MacFJA\RediSearch\Builder
-{
-    public const STRUCTURE_HASH = 'HASH';
-
-    public const MAXIMUM_ALLOWED_FIELDS = 1024;
-
-    public const MAXIMUM_ALLOWED_TEXT_FIELDS = 128;
-
-    public const SUPPORTED_LANGUAGES = [
-        'arabic', 'danish', 'dutch', 'english', 'finnish', 'french', 'german', 'hungarian', 'italian', 'norwegian',
-        'portuguese', 'romanian', 'russian', 'spanish', 'swedish', 'tamil', 'turkish', 'chinese',
-    ];
-
-    /** @var Client */
-    private $redis;
-
-    /** @var array<Field> */
-    private $fields = [];
-
-    /** @var null|string */
-    private $name;
-
-    /** @var null|string */
-    private $structure;
-
-    /** @var array<string> */
-    private $prefix = [];
-
-    /** @var null|string */
-    private $filter;
-
-    /** @var null|string */
-    private $language;
-
-    /** @var null|string */
-    private $languageField;
-
-    /** @var null|float */
-    private $score;
-
-    /** @var null|string */
-    private $scoreField;
-
-    /** @var null|string */
-    private $payloadField;
-
-    /** @var null|int */
-    private $maxTextFields;
-
-    /** @var bool */
-    private $noOffsets = false;
-
-    /** @var null|int */
-    private $temporary;
-
-    /** @var bool */
-    private $noHighlight = false;
-
-    /** @var bool */
-    private $noFields = false;
-
-    /** @var bool */
-    private $noFrequencies = false;
-
-    /** @var array<string> */
-    private $stopWords = [];
-
-    /** @var bool */
-    private $skipInitialScan = false;
-
-    public function __construct(Client $redis)
-    {
-        $this->redis = $redis;
-        $this->reset();
-    }
-
-    public function withName(string $name): self
-    {
-        $this->name = $name;
-
-        return $this;
-    }
-
-    public function withHashStructure(): self
-    {
-        $this->structure = self::STRUCTURE_HASH;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $prefix
-     *
-     * @throws Throwable
-     *
-     * @return $this
-     */
-    public function withPrefix(array $prefix): self
-    {
-        DataHelper::assert(count($prefix) > 0);
-        DataHelper::assertArrayOf($prefix, 'string');
-        $this->prefix = $prefix;
-
-        return $this;
-    }
-
-    public function withFilter(string $expression): self
-    {
-        $this->filter = $expression;
-
-        return $this;
-    }
-
-    public function withLanguage(string $language): self
-    {
-        DataHelper::assert(in_array($language, self::SUPPORTED_LANGUAGES, true), UnsupportedLanguageException::class);
-        $this->language = $language;
-
-        return $this;
-    }
-
-    public function withLanguageField(string $fieldName): self
-    {
-        $this->languageField = $fieldName;
-
-        return $this;
-    }
-
-    public function withDefaultScore(float $score): self
-    {
-        $this->score = $score;
-
-        return $this;
-    }
-
-    public function withScoreField(string $fieldName): self
-    {
-        $this->scoreField = $fieldName;
-
-        return $this;
-    }
-
-    public function withPayloadField(string $fieldName): self
-    {
-        $this->payloadField = $fieldName;
-
-        return $this;
-    }
-
-    public function withMaxTextFields(int $max): self
-    {
-        $this->maxTextFields = $max;
-
-        return $this;
-    }
-
-    public function withNoOffsets(bool $flag = true): self
-    {
-        $this->noOffsets = $flag;
-
-        return $this;
-    }
-
-    public function withTemporary(int $ttl): self
-    {
-        $this->temporary = $ttl;
-
-        return $this;
-    }
-
-    public function withNoHighlight(bool $flag = true): self
-    {
-        $this->noHighlight = $flag;
-
-        return $this;
-    }
-
-    public function withNoFields(bool $flag = true): self
-    {
-        $this->noFields = $flag;
-
-        return $this;
-    }
-
-    public function withNoFrequencies(bool $flag = true): self
-    {
-        $this->noFrequencies = $flag;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $words
-     *
-     * @return $this
-     */
-    public function withStopWords(array $words): self
-    {
-        DataHelper::assert(count($words) > 0);
-        DataHelper::assertArrayOf($words, 'string');
-        $this->stopWords = $words;
-
-        return $this;
-    }
-
-    public function withSkipInitialScan(bool $flag = true): self
-    {
-        $this->skipInitialScan = $flag;
-
-        return $this;
-    }
-
-    public function reset(): \MacFJA\RediSearch\Builder
-    {
-        $this->fields = [];
-        $this->name = null;
-        $this->structure = null;
-        $this->prefix = [];
-        $this->filter = null;
-        $this->language = null;
-        $this->languageField = null;
-        $this->score = null;
-        $this->scoreField = null;
-        $this->payloadField = null;
-        $this->maxTextFields = null;
-        $this->noOffsets = false;
-        $this->temporary = null;
-        $this->noHighlight = false;
-        $this->noFields = false;
-        $this->noFrequencies = false;
-        $this->stopWords = [];
-        $this->skipInitialScan = false;
-
-        return $this;
-    }
-
-    /**
-     * @deprecated Use \MacFJA\RediSearch\Index\Builder::execute()
-     */
-    public function create(): bool
-    {
-        return $this->execute();
-    }
-
-    public function execute(): bool
-    {
-        $this->validateData();
-        $query = $this->createRequest();
-        $result = $this->redis->executeRaw($query);
-
-        if ('OK' === $result) {
-            $this->reset();
-
-            return true;
-        }
-
-        return false;
-    }
-
-    public function addField(Field $field): self
-    {
-        $this->fields[] = $field;
-
-        return $this;
-    }
-
-    public function addTextField(string $name, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false): self
-    {
-        return $this->addField(
-            new TextField($name, $noStem, $weight, $phonetic, $sortable, $noIndex)
-        );
-    }
-
-    public function addGeoField(string $name, bool $noIndex = false): self
-    {
-        return $this->addField(new GeoField($name, $noIndex));
-    }
-
-    public function addNumericField(string $name, bool $sortable = false, bool $noIndex = false): self
-    {
-        return $this->addField(new NumericField($name, $sortable, $noIndex));
-    }
-
-    public function addTagField(string $name, ?string $separator, bool $sortable = false, bool $noIndex = false): self
-    {
-        return $this->addField(new TagField($name, $separator, $sortable, $noIndex));
-    }
-
-    /**
-     * @return array<float|int|string>
-     */
-    private function createRequest(): array
-    {
-        assert(is_string($this->name));
-
-        $query = ['FT.CREATE', $this->name];
-        if (is_string($this->structure)) {
-            $query[] = 'ON';
-            $query[] = $this->structure;
-        }
-        $query = RedisHelper::buildQueryList($query, [
-            'PREFIX' => $this->prefix,
-            'STOPWORDS' => $this->stopWords,
-        ]);
-        $query = RedisHelper::buildQueryBoolean($query, [
-            'NOOFFSETS' => $this->noOffsets,
-            'NOHL' => $this->noHighlight,
-            'NOFIELDS' => $this->noFields,
-            'NOFREQS' => $this->noFrequencies,
-            'SKIPINITIALSCAN' => $this->skipInitialScan,
-        ]);
-        $query = RedisHelper::buildQueryNotNull($query, [
-            'FILTER' => $this->filter,
-            'LANGUAGE' => $this->language,
-            'LANGUAGE_FIELD' => $this->languageField,
-            'SCORE' => $this->score,
-            'SCORE_FIELD' => $this->scoreField,
-            'PAYLOAD_FIELD' => $this->payloadField,
-            'MAXTEXTFIELDS' => $this->maxTextFields,
-            'TEMPORARY' => $this->temporary,
-        ]);
-        if (count($this->fields) > 0) {
-            $query[] = 'SCHEMA';
-
-            foreach ($this->fields as $field) {
-                /** @var Field $field */
-                $query = array_merge($query, $field->getQueryParts());
-            }
-        }
-
-        return $query;
-    }
-
-    private function validateData(): bool
-    {
-        if (null === $this->name) {
-            throw new MissingNameException();
-        }
-        if (count($this->fields) > self::MAXIMUM_ALLOWED_FIELDS) {
-            throw new TooManyFields('Maximum allowed fields is 1024');
-        }
-        if (count(array_filter($this->fields, function (Field $field) {
-            return Field::TYPE_TEXT === $field->getType();
-        })) > self::MAXIMUM_ALLOWED_TEXT_FIELDS) {
-            throw new TooManyFields('Maximum allowed Text field is 128');
-        }
-
-        return true;
-    }
-}
diff --git a/src/Index/Builder/AbstractField.php b/src/Index/Builder/AbstractField.php
deleted file mode 100644
index 7a01d90..0000000
--- a/src/Index/Builder/AbstractField.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Index\Builder;
-
-use function array_merge;
-use function implode;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use SGH\Comparable\ComparatorException;
-
-abstract class AbstractField implements Field
-{
-    /** @var string */
-    private $name;
-
-    /** @var bool */
-    private $sortable = false;
-
-    /** @var bool */
-    private $noIndex = false;
-
-    public function __construct(string $name, bool $sortable = false, bool $noIndex = false)
-    {
-        $this->name = $name;
-        $this->sortable = $sortable;
-        $this->noIndex = $noIndex;
-    }
-
-    public function isNoStem(): bool
-    {
-        return false;
-    }
-
-    public function getPhonetic(): ?string
-    {
-        return null;
-    }
-
-    public function getWeight(): ?float
-    {
-        return null;
-    }
-
-    public function getSeparator(): ?string
-    {
-        return null;
-    }
-
-    public function isSortable(): bool
-    {
-        return $this->sortable;
-    }
-
-    public function isNoIndex(): bool
-    {
-        return $this->noIndex;
-    }
-
-    public function getName(): string
-    {
-        return $this->name;
-    }
-
-    public function getQueryParts(): array
-    {
-        $query = array_merge(
-            [$this->name, $this->getType()],
-            $this->getAdditionalQueryParts()
-        );
-
-        return RedisHelper::buildQueryBoolean($query, [
-            'SORTABLE' => $this->sortable,
-            'NOINDEX' => $this->noIndex,
-        ]);
-    }
-
-    public function compareTo($object)
-    {
-        if (!($object instanceof Field)) {
-            throw new ComparatorException();
-        }
-
-        return implode(' ', $object->getQueryParts()) <=> implode(' ', $this->getQueryParts());
-    }
-
-    /**
-     * @return array<float|int|string>
-     */
-    protected function getAdditionalQueryParts(): array
-    {
-        return [];
-    }
-}
diff --git a/src/Index/Builder/Exception/TooManyFields.php b/src/Index/Builder/Exception/TooManyFields.php
deleted file mode 100644
index f9f8618..0000000
--- a/src/Index/Builder/Exception/TooManyFields.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Index\Builder\Exception;
-
-use OutOfBoundsException;
-
-class TooManyFields extends OutOfBoundsException
-{
-}
diff --git a/src/Index/Builder/Exception/UnsupportedLanguageException.php b/src/Index/Builder/Exception/UnsupportedLanguageException.php
deleted file mode 100644
index 2feac01..0000000
--- a/src/Index/Builder/Exception/UnsupportedLanguageException.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Index\Builder\Exception;
-
-use UnexpectedValueException;
-
-class UnsupportedLanguageException extends UnexpectedValueException
-{
-}
diff --git a/src/Index/Builder/Field.php b/src/Index/Builder/Field.php
deleted file mode 100644
index bbf1961..0000000
--- a/src/Index/Builder/Field.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Index\Builder;
-
-use MacFJA\RediSearch\PartialQuery;
-use SGH\Comparable\Comparable;
-
-interface Field extends PartialQuery, Comparable
-{
-    public const TYPE_TEXT = 'TEXT';
-
-    public const TYPE_NUMERIC = 'NUMERIC';
-
-    public const TYPE_GEO = 'GEO';
-
-    public const TYPE_TAG = 'TAG';
-
-    public function getName(): string;
-
-    public function getType(): string;
-
-    public function isSortable(): bool;
-
-    public function isNoStem(): bool;
-
-    public function isNoIndex(): bool;
-
-    public function getPhonetic(): ?string;
-
-    public function getWeight(): ?float;
-
-    public function getSeparator(): ?string;
-}
diff --git a/src/Index/InfoResult.php b/src/Index/InfoResult.php
deleted file mode 100644
index 3b94b3a..0000000
--- a/src/Index/InfoResult.php
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Index;
-
-use function array_map;
-use function array_shift;
-use function assert;
-use function in_array;
-use function is_array;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Index\Builder\Field;
-use MacFJA\RediSearch\Index\Builder\GeoField;
-use MacFJA\RediSearch\Index\Builder\NumericField;
-use MacFJA\RediSearch\Index\Builder\TagField;
-use MacFJA\RediSearch\Index\Builder\TextField;
-use function sprintf;
-use UnexpectedValueException;
-
-class InfoResult
-{
-    /** @var int */
-    protected $maxDocumentId;
-
-    /** @var string */
-    private $indexName;
-
-    /** @var array<string> */
-    private $indexOptions = [];
-
-    /** @var array<array<string>|string> */
-    private $definition = [];
-
-    /** @var array<mixed> */
-    private $fields = [];
-
-    /** @var int */
-    private $documentCount;
-
-    /** @var int */
-    private $termsCount;
-
-    /** @var float */
-    private $averageRecordSize;
-
-    /** @var float */
-    private $indexSize;
-
-    /** @var int */
-    private $indexBlockCount;
-
-    /** @var bool */
-    private $indexing;
-
-    /** @var int */
-    private $indexingFailureCount;
-
-    /** @var float */
-    private $percentIndexed;
-
-    /** @var array<string> */
-    private $stopWords;
-
-    /**
-     * @param array<string>               $indexOptions
-     * @param array<array<string>|string> $definition
-     * @param array<mixed>                $fields
-     * @param array<string>               $stopWords
-     *
-     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
-     */
-    public function __construct(string $indexName, array $indexOptions, array $definition, array $fields, int $documentCount, int $maxDocumentId, int $termsCount, float $averageRecordSize, float $indexSize, int $indexBlockCount, bool $indexing, int $indexingFailureCount, float $percentIndexed, array $stopWords)
-    {
-        /** @var array<string> $prefixes */
-        $prefixes = $definition['prefixes'] ?? [];
-        assert(is_array($prefixes));
-        unset($definition['prefixes']);
-        /** @var array<string> $definition */
-        DataHelper::assertArrayOf($indexOptions, 'string');
-        DataHelper::assertArrayOf($definition, 'string');
-        DataHelper::assertArrayOf($prefixes, 'string');
-        DataHelper::assertArrayOf($stopWords, 'string');
-        $definition['prefixes'] = $prefixes;
-
-        $this->indexName = $indexName;
-        $this->indexOptions = $indexOptions;
-        $this->definition = $definition;
-        $this->fields = $fields;
-        $this->documentCount = $documentCount;
-        $this->maxDocumentId = $maxDocumentId;
-        $this->termsCount = $termsCount;
-        $this->averageRecordSize = $averageRecordSize;
-        $this->indexSize = $indexSize;
-        $this->indexBlockCount = $indexBlockCount;
-        $this->indexing = $indexing;
-        $this->indexingFailureCount = $indexingFailureCount;
-        $this->percentIndexed = $percentIndexed;
-        $this->stopWords = $stopWords;
-    }
-
-    public function getIndexName(): string
-    {
-        return $this->indexName;
-    }
-
-    /**
-     * @return array<string>
-     */
-    public function getIndexOptions(): array
-    {
-        return $this->indexOptions;
-    }
-
-    /**
-     * @return array<array<string>|string>
-     */
-    public function getDefinition(): array
-    {
-        return $this->definition;
-    }
-
-    /**
-     * @return array<mixed>
-     */
-    public function getFields(): array
-    {
-        return $this->fields;
-    }
-
-    /**
-     * @return array<Field>
-     */
-    public function getFieldsAsObject(): array
-    {
-        return array_map(function (array $field): Field {
-            $name = (string) array_shift($field);
-            array_shift($field); // type
-            $type = (string) array_shift($field);
-
-            switch ($type) {
-                case 'GEO':
-                    return new GeoField(
-                        $name,
-                        in_array('NOINDEX', $field, true)
-                    );
-                case 'NUMERIC':
-                    return new NumericField(
-                        $name,
-                        in_array('SORTABLE', $field, true),
-                        in_array('NOINDEX', $field, true)
-                    );
-                case 'TAG':
-                    return new TagField(
-                        $name,
-                        /** @phan-suppress-next-line PhanPartialTypeMismatchArgument */
-                        DataHelper::nullOrCast(RedisHelper::getValue($field, 'SEPARATOR'), 'string'),
-                        in_array('SORTABLE', $field, true),
-                        in_array('NOINDEX', $field, true)
-                    );
-                case 'TEXT':
-                    return new TextField(
-                        $name,
-                        in_array('NOSTEM', $field, true),
-                        /** @phan-suppress-next-line PhanPartialTypeMismatchArgument */
-                        DataHelper::nullOrCast(RedisHelper::getValue($field, 'WEIGHT'), 'float'),
-                        /** @phan-suppress-next-line PhanPartialTypeMismatchArgument */
-                        DataHelper::nullOrCast(RedisHelper::getValue($field, 'PHONETIC'), 'string'),
-                        in_array('SORTABLE', $field, true),
-                        in_array('NOINDEX', $field, true)
-                    );
-                default:
-                    throw new UnexpectedValueException(sprintf('Unknown field type "%s" for field "%s"', $type, $name));
-            }
-        }, $this->fields);
-    }
-
-    public function getDocumentCount(): int
-    {
-        return $this->documentCount;
-    }
-
-    public function getMaxDocumentId(): int
-    {
-        return $this->maxDocumentId;
-    }
-
-    public function getTermsCount(): int
-    {
-        return $this->termsCount;
-    }
-
-    public function getAverageRecordSize(): float
-    {
-        return $this->averageRecordSize;
-    }
-
-    public function getIndexSize(): float
-    {
-        return $this->indexSize;
-    }
-
-    public function getIndexBlockCount(): int
-    {
-        return $this->indexBlockCount;
-    }
-
-    public function isIndexing(): bool
-    {
-        return $this->indexing;
-    }
-
-    public function getIndexingFailureCount(): int
-    {
-        return $this->indexingFailureCount;
-    }
-
-    public function getPercentIndexed(): float
-    {
-        return $this->percentIndexed;
-    }
-
-    /**
-     * @return array<string>
-     */
-    public function getStopWords(): array
-    {
-        return $this->stopWords;
-    }
-}
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
new file mode 100644
index 0000000..456c057
--- /dev/null
+++ b/src/IndexBuilder.php
@@ -0,0 +1,323 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch;
+
+use BadMethodCallException;
+use function count;
+use function in_array;
+use InvalidArgumentException;
+use function is_array;
+use function is_float;
+use function is_int;
+use function is_string;
+use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
+use MacFJA\RediSearch\Redis\Command\Create;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
+use Predis\ClientInterface;
+use Predis\Response\ResponseInterface;
+use RuntimeException;
+use function strlen;
+
+/**
+ * @method IndexBuilder setIndex(string $name)
+ * @method IndexBuilder withIndex(string $name)
+ * @method IndexBuilder setStructure(string $type)
+ * @method IndexBuilder withStructure(string $type)
+ * @method IndexBuilder setPrefixes(array $prefixes)
+ * @method IndexBuilder withPrefixes(array $prefixes)
+ * @method IndexBuilder addPrefix(string ...$prefixes)
+ * @method IndexBuilder withAddedPrefix(string ...$prefixes)
+ * @method IndexBuilder setFilter(string $filter)
+ * @method IndexBuilder withFilter(string $filter)
+ * @method IndexBuilder setDefaultLanguage(string $language)
+ * @method IndexBuilder withDefaultLanguage(string $language)
+ * @method IndexBuilder setLanguageField(string $field)
+ * @method IndexBuilder withLanguageField(string $field)
+ * @method IndexBuilder setDefaultScore(float $score)
+ * @method IndexBuilder withDefaultScore(float $score)
+ * @method IndexBuilder setScoreField(string $field)
+ * @method IndexBuilder withScoreField(string $field)
+ * @method IndexBuilder setPayloadField(string $field)
+ * @method IndexBuilder withPayloadField(string $field)
+ * @method IndexBuilder setMaxTextFields(bool $active)
+ * @method IndexBuilder withMaxTextFields(bool $active)
+ * @method IndexBuilder setTemporary(int $seconds)
+ * @method IndexBuilder withTemporary(int $seconds)
+ * @method IndexBuilder setNoOffsets(bool $active)
+ * @method IndexBuilder withNoOffsets(bool $active)
+ * @method IndexBuilder setNoHighLight(bool $active)
+ * @method IndexBuilder withNoHighLight(bool $active)
+ * @method IndexBuilder setNoFields(bool $active)
+ * @method IndexBuilder withNoFields(bool $active)
+ * @method IndexBuilder setNoFrequencies(bool $active)
+ * @method IndexBuilder withNoFrequencies(bool $active)
+ * @method IndexBuilder setSkipInitialScan(bool $active)
+ * @method IndexBuilder withSkipInitialScan(bool $active)
+ * @method IndexBuilder setStopWords(array $words)
+ * @method IndexBuilder withStopWords(array $words)
+ * @method IndexBuilder addStopWord(string ...$words)
+ * @method IndexBuilder withAddedStopWord(string ...$words)
+ * @method IndexBuilder setFields(array $fields)
+ * @method IndexBuilder withFields(array $fields)
+ * @method IndexBuilder withAddedField(CreateCommandFieldOption ...$fields)
+ * @method IndexBuilder withAddedTextField(string $name, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false)
+ * @method IndexBuilder withAddedNumericField(string $name, bool $sortable = false, bool $noIndex = false)
+ * @method IndexBuilder withAddedGeoField(string $name, bool $noIndex = false)
+ * @method IndexBuilder withAddedTagField(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false)
+ *
+ * @SuppressWarnings(PHPMD.TooManyFields)
+ */
+class IndexBuilder
+{
+    use AddFieldOptionTrait;
+
+    private const IS_ARRAY = ['prefixes', 'stopWords', 'fields'];
+    /** @var null|string */
+    private $index;
+    /** @var null|string */
+    private $structure;
+    /** @var array<string> */
+    private $prefixes = [];
+    /** @var null|string */
+    private $filter;
+    /** @var null|string */
+    private $defaultLanguage;
+    /** @var null|string */
+    private $languageField;
+    /** @var null|float */
+    private $defaultScore;
+    /** @var null|string */
+    private $scoreField;
+    /** @var null|string */
+    private $payloadField;
+    /** @var bool */
+    private $maxTextFields = false;
+    /** @var null|int */
+    private $temporary;
+    /** @var bool */
+    private $noOffsets = false;
+    /** @var bool */
+    private $noHighLight = false;
+    /** @var bool */
+    private $noFields = false;
+    /** @var bool */
+    private $noFrequencies = false;
+    /** @var bool */
+    private $skipInitialScan = false;
+    /** @var null|array<string> */
+    private $stopWords;
+    /** @var array<CreateCommandFieldOption> */
+    private $fields = [];
+
+    /**
+     * @param array<mixed> $arguments
+     *
+     * @return $this
+     */
+    public function __call(string $name, array $arguments): self
+    {
+        $calls = ['set' => true, 'add' => false, 'withAdded' => false, 'with' => false];
+        foreach ($calls as $type => $singleArgument) {
+            $result = $this->doCall($type, $name, true === $singleArgument ? $arguments[0] ?? null : $arguments);
+            if ($result instanceof IndexBuilder) {
+                return $result;
+            }
+        }
+
+        throw new BadMethodCallException(sprintf('Call undefined %s method', $name));
+    }
+
+    public function setNoStopWords(): self
+    {
+        $this->stopWords = [];
+
+        return $this;
+    }
+
+    public function withNoStopWords(): self
+    {
+        $newInstance = clone $this;
+
+        return $newInstance->setNoStopWords();
+    }
+
+    public function addField(CreateCommandFieldOption ...$option): self
+    {
+        array_push($this->fields, ...$option);
+
+        return $this;
+    }
+
+    public function create(ClientInterface $client, string $rediSearchVersion = '2.0.0'): ResponseInterface
+    {
+        $command = $this->getCommand($rediSearchVersion);
+
+        return $client->executeCommand($command);
+    }
+
+    /**
+     * @throws RuntimeException if the index is not defined
+     *
+     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+     * @SuppressWarnings(PHPMD.NPathComplexity)
+     */
+    public function getCommand(string $rediSearchVersion = '2.0.0'): Create
+    {
+        if (!is_string($this->index)) {
+            throw new RuntimeException('The index name is missing');
+        }
+        $command = new Create($rediSearchVersion);
+        $command
+            ->setIndex($this->index)
+            ->setMaxTextFields($this->maxTextFields)
+            ->setNoOffsets($this->noOffsets)
+            ->setNoHighLight($this->noHighLight)
+            ->setNoFields($this->noFields)
+            ->setNoFrequencies($this->noFrequencies)
+            ->setSkipInitialScan($this->skipInitialScan)
+        ;
+        if (is_string($this->structure)) {
+            $command->setStructure($this->structure);
+        }
+        if (count($this->prefixes) > 0) {
+            $command->setPrefixes(...$this->prefixes);
+        }
+        if (is_string($this->filter)) {
+            $command->setFilter($this->filter);
+        }
+        if (is_string($this->defaultLanguage)) {
+            $command->setDefaultLanguage($this->defaultLanguage);
+        }
+        if (is_string($this->languageField)) {
+            $command->setLanguageField($this->languageField);
+        }
+        if (is_float($this->defaultScore)) {
+            $command->setDefaultScore($this->defaultScore);
+        }
+        if (is_string($this->scoreField)) {
+            $command->setScoreField($this->scoreField);
+        }
+        if (is_string($this->payloadField)) {
+            $command->setPayloadField($this->payloadField);
+        }
+        if (is_int($this->temporary)) {
+            $command->setTemporary($this->temporary);
+        }
+        if (is_array($this->stopWords) && count($this->stopWords) > 0) {
+            $command->setStopWords(...$this->stopWords);
+        }
+        if (is_array($this->stopWords) && 0 === count($this->stopWords)) {
+            $command->setNoStopWords();
+        }
+        foreach ($this->fields as $field) {
+            $command->addField($field);
+        }
+
+        return $command;
+    }
+
+    /**
+     * @param array<mixed>|mixed $arguments
+     *
+     * @return null|$this
+     */
+    private function doCall(string $type, string $name, $arguments): ?self
+    {
+        $typeLength = strlen($type);
+        if (0 === strpos($name, $type) && $name[$typeLength] === strtoupper($name[$typeLength])) {
+            return $this->{$type.'Call'}(substr($name, $typeLength), $arguments);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param mixed $argument
+     *
+     * @return $this
+     *
+     * @SuppressWarnings(PHPMD.UnusedPrivateMethod) -- Called in doCall
+     */
+    private function withCall(string $name, $argument): self
+    {
+        $newInstance = clone $this;
+
+        return $newInstance->{'set'.$name}($name, $argument);
+    }
+
+    /**
+     * @param array<mixed> $arguments
+     *
+     * @return $this
+     *
+     * @SuppressWarnings(PHPMD.UnusedPrivateMethod) -- Called in doCall
+     */
+    private function withAddedCall(string $name, array $arguments): self
+    {
+        $newInstance = clone $this;
+
+        return $newInstance->{'add'.$name}($name, $arguments);
+    }
+
+    /**
+     * @param mixed $argument
+     *
+     * @return $this
+     *
+     * @SuppressWarnings(PHPMD.UnusedPrivateMethod) -- Called in doCall
+     */
+    private function setCall(string $name, $argument): self
+    {
+        $propertyName = lcfirst($name);
+        if (property_exists($this, $propertyName)) {
+            $this->{$propertyName} = $argument;
+
+            return $this;
+        }
+
+        throw new BadMethodCallException(sprintf('Call undefined set%s method', $name));
+    }
+
+    /**
+     * @param array<mixed> $arguments
+     *
+     * @return $this
+     *
+     * @SuppressWarnings(PHPMD.UnusedPrivateMethod) -- Called in doCall
+     */
+    private function addCall(string $name, array $arguments): self
+    {
+        $propertyName = lcfirst($name);
+
+        if (!in_array($propertyName, self::IS_ARRAY, true)) {
+            throw new BadMethodCallException(sprintf('Call undefined add%s method', $name));
+        }
+
+        if (0 === count($arguments)) {
+            throw new InvalidArgumentException(sprintf('add%s method need at least one parameter', $name));
+        }
+
+        array_push($this->{$propertyName}, ...$arguments);
+
+        return $this;
+    }
+}
diff --git a/src/Pipeable.php b/src/Pipeable.php
deleted file mode 100644
index a5b13b0..0000000
--- a/src/Pipeable.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch;
-
-use MacFJA\RediSearch\Helper\PipelineItem;
-
-interface Pipeable
-{
-    public function asPipelineItem(): PipelineItem;
-}
diff --git a/src/Query/Builder.php b/src/Query/Builder.php
new file mode 100644
index 0000000..36c9a26
--- /dev/null
+++ b/src/Query/Builder.php
@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query;
+
+use MacFJA\RediSearch\Query\Builder\AndGroup;
+use MacFJA\RediSearch\Query\Builder\ExactMatch;
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use MacFJA\RediSearch\Query\Builder\NumericFacet;
+use MacFJA\RediSearch\Query\Builder\QueryElement;
+use MacFJA\RediSearch\Query\Builder\QueryElementGroup;
+use MacFJA\RediSearch\Query\Builder\TagFacet;
+use MacFJA\RediSearch\Query\Builder\TextFacet;
+
+class Builder implements QueryElement, QueryElementGroup
+{
+    /** @var AndGroup */
+    private $group;
+
+    public function __construct()
+    {
+        $this->group = new AndGroup();
+    }
+
+    public function addTextFacet(string $field, string ...$values): self
+    {
+        $this->group->addElement(
+            new TextFacet([$field], ...$values)
+        );
+
+        return $this;
+    }
+
+    public function addTagFacet(string $tagName, string ...$values): self
+    {
+        $this->group->addElement(
+            new TagFacet([$tagName], ...$values)
+        );
+
+        return $this;
+    }
+
+    public function addNumericFacet(string $fieldName, ?float $min, ?float $max): self
+    {
+        $this->group->addElement(
+            new NumericFacet([$fieldName], $min, $max)
+        );
+
+        return $this;
+    }
+
+    public function addGeoFacet(string $fieldName, float $lon, float $lat, float $radius, string $unit): self
+    {
+        $this->group->addElement(
+            new GeoFacet([$fieldName], $lon, $lat, $radius, $unit)
+        );
+
+        return $this;
+    }
+
+    public function addString(string ...$strings): self
+    {
+        foreach ($strings as $string) {
+            $this->group->addElement(new ExactMatch($string));
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return $this
+     */
+    public function addElement(QueryElement $element): QueryElementGroup
+    {
+        $this->group->addElement($element);
+
+        return $this;
+    }
+
+    public function render(?callable $escaper = null): string
+    {
+        return $this->group->render(null);
+    }
+
+    public function priority(): int
+    {
+        return self::PRIORITY_BEFORE;
+    }
+
+    public function includeSpace(): bool
+    {
+        return false;
+    }
+}
diff --git a/src/Search/QueryBuilder/OrGroup.php b/src/Query/Builder/AbstractGroup.php
similarity index 52%
rename from src/Search/QueryBuilder/OrGroup.php
rename to src/Query/Builder/AbstractGroup.php
index ec3dada..cc276b8 100644
--- a/src/Search/QueryBuilder/OrGroup.php
+++ b/src/Query/Builder/AbstractGroup.php
@@ -19,44 +19,30 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use function array_map;
-use function count;
-use function implode;
-use function sprintf;
-use function substr;
-use function usort;
-
-class OrGroup implements GroupPartialQuery
+abstract class AbstractGroup implements QueryElement, QueryElementGroup
 {
-    /** @var array<PartialQuery> */
-    private $expressions = [];
+    /**
+     * @var array<QueryElement>
+     */
+    protected $elements;
 
-    public function render(): string
+    /**
+     * @param array<QueryElement> $elements
+     */
+    final public function __construct(array $elements = [])
     {
-        usort($this->expressions, function (PartialQuery $queryA, PartialQuery $queryB) {
-            return $queryA->priority() <=> $queryB->priority();
-        });
-        $terms = array_map(function (PartialQuery $query): string {
-            if ($query->includeSpace()) {
-                return sprintf('(%s)', $query->render());
-            }
-
-            return $query->render();
-        }, $this->expressions);
-
-        return sprintf('(%s)', implode('|', $terms));
+        $this->elements = $elements;
+        $this->sortElement();
     }
 
-    public function includeSpace(): bool
+    public function addElement(QueryElement $element): QueryElementGroup
     {
-        return false;
-    }
+        $this->elements[] = $element;
+        $this->sortElement();
 
-    public function addExpression(PartialQuery $expression): void
-    {
-        $this->expressions[] = $expression;
+        return $this;
     }
 
     public function priority(): int
@@ -64,18 +50,31 @@ public function priority(): int
         return self::PRIORITY_NORMAL;
     }
 
-    public static function renderNoParentheses(PartialQuery ...$queries): string
+    public function includeSpace(): bool
     {
-        if (0 === count($queries)) {
-            return '';
+        /** @var QueryElement $element */
+        foreach ($this->elements as $element) {
+            if ($element->includeSpace()) {
+                return true;
+            }
         }
 
-        $group = new self();
-        foreach ($queries as $query) {
-            $group->addExpression($query);
-        }
-        $rendered = $group->render();
+        return false;
+    }
+
+    public static function fromStrings(string ...$words): self
+    {
+        $words = array_map(static function (string $word): ExactMatch {
+            return new ExactMatch($word);
+        }, $words);
+
+        return new static($words);
+    }
 
-        return substr($rendered, 1, -1) ?: $rendered;
+    private function sortElement(): void
+    {
+        usort($this->elements, static function (QueryElement $elementA, QueryElement $elementB) {
+            return $elementA->priority() <=> $elementB->priority();
+        });
     }
 }
diff --git a/src/Query/Builder/AndGroup.php b/src/Query/Builder/AndGroup.php
new file mode 100644
index 0000000..9072f67
--- /dev/null
+++ b/src/Query/Builder/AndGroup.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+use function count;
+
+class AndGroup extends AbstractGroup
+{
+    public function render(?callable $escaper = null): string
+    {
+        if (1 === count($this->elements)) {
+            return (reset($this->elements))->render($escaper);
+        }
+
+        return implode(' ', array_map(static function (QueryElement $element) use ($escaper): string {
+            if ($element instanceof OrGroup) {
+                return (new EncapsulationGroup('(', ')', $element))->render($escaper);
+            }
+
+            return $element->render($escaper);
+        }, $this->elements));
+    }
+
+    public function includeSpace(): bool
+    {
+        return count($this->elements) > 1 || parent::includeSpace();
+    }
+}
diff --git a/src/Search/QueryBuilder/TagFacet.php b/src/Query/Builder/EncapsulationGroup.php
similarity index 58%
rename from src/Search/QueryBuilder/TagFacet.php
rename to src/Query/Builder/EncapsulationGroup.php
index 57ccf15..656d535 100644
--- a/src/Search/QueryBuilder/TagFacet.php
+++ b/src/Query/Builder/EncapsulationGroup.php
@@ -19,37 +19,39 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use function array_map;
-use function count;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use MacFJA\RediSearch\Search\Exception\NotEnoughTermsException;
-use function sprintf;
-
-class TagFacet implements PartialQuery
+class EncapsulationGroup implements QueryElement, QueryElementDecorator
 {
     /** @var string */
-    private $field;
-
-    /** @var array<string> */
-    private $orValues;
+    private $open;
+    /** @var string */
+    private $close;
+    /** @var bool */
+    private $omitIfNoSpace;
+    /** @var QueryElement */
+    private $element;
 
-    public function __construct(string $field, string ...$orValues)
+    public function __construct(string $open, string $close, QueryElement $element, bool $omitIfNoSpace = false)
     {
-        DataHelper::assert(count($orValues) > 0, NotEnoughTermsException::class);
-        $this->field = $field;
-        $this->orValues = $orValues;
+        $this->open = $open;
+        $this->close = $close;
+        $this->omitIfNoSpace = $omitIfNoSpace;
+        $this->element = $element;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
-        $terms = OrGroup::renderNoParentheses(...array_map(function (string $orValue) {
-            return new Word($orValue);
-        }, $this->orValues));
+        if ($this->omitIfNoSpace && !$this->element->includeSpace()) {
+            return $this->element->render($escaper);
+        }
+
+        return sprintf('%s%s%s', $this->open, $this->element->render($escaper), $this->close);
+    }
 
-        return sprintf('@%s:{%s}', EscapeHelper::escapeFieldName($this->field), $terms);
+    public function priority(): int
+    {
+        return self::PRIORITY_NORMAL;
     }
 
     public function includeSpace(): bool
@@ -57,8 +59,8 @@ public function includeSpace(): bool
         return false;
     }
 
-    public function priority(): int
+    public static function simple(QueryElement $element): self
     {
-        return self::PRIORITY_NORMAL;
+        return new self('(', ')', $element, true);
     }
 }
diff --git a/src/Search/QueryBuilder/ExactMatch.php b/src/Query/Builder/ExactMatch.php
similarity index 70%
rename from src/Search/QueryBuilder/ExactMatch.php
rename to src/Query/Builder/ExactMatch.php
index b6fa311..c665e65 100644
--- a/src/Search/QueryBuilder/ExactMatch.php
+++ b/src/Query/Builder/ExactMatch.php
@@ -19,38 +19,36 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use function sprintf;
-use function strpos;
+use MacFJA\RediSearch\Query\Escaper;
 
-class ExactMatch implements PartialQuery
+class ExactMatch implements QueryElement
 {
     /** @var string */
-    private $match;
+    private $word;
 
-    public function __construct(string $match)
+    public function __construct(string $word)
     {
-        $this->match = $match;
+        $this->word = $word;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
-        if (false === strpos($this->match, ' ')) {
-            return EscapeHelper::escapeWord($this->match);
+        if (false === strpos($this->word, ' ')) {
+            return $this->word;
         }
 
-        return sprintf('"%s"', EscapeHelper::escapeExactMatch($this->match));
+        return sprintf('"%s"', Escaper::escapeExactMatch($this->word));
     }
 
-    public function includeSpace(): bool
+    public function priority(): int
     {
-        return false;
+        return self::PRIORITY_NORMAL;
     }
 
-    public function priority(): int
+    public function includeSpace(): bool
     {
-        return self::PRIORITY_BEFORE;
+        return false;
     }
 }
diff --git a/src/Search/QueryBuilder/Negation.php b/src/Query/Builder/FieldFacet.php
similarity index 63%
rename from src/Search/QueryBuilder/Negation.php
rename to src/Query/Builder/FieldFacet.php
index 0782829..2a6b09c 100644
--- a/src/Search/QueryBuilder/Negation.php
+++ b/src/Query/Builder/FieldFacet.php
@@ -19,43 +19,42 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use function ctype_digit;
-use function sprintf;
-use function substr;
+use MacFJA\RediSearch\Query\Escaper;
 
-class Negation implements PartialQuery
+class FieldFacet implements QueryElement
 {
-    private const WITH_SPACE_PATTERN = '-(%s)';
-
-    private const WITHOUT_SPACE_PATTERN = '-%s';
-
-    /** @var PartialQuery */
-    private $expression;
-
-    public function __construct(PartialQuery $expression)
+    /** @var array<string> */
+    private $fields;
+    /** @var QueryElement */
+    private $element;
+
+    /**
+     * @param array<string> $fields
+     */
+    public function __construct(array $fields, QueryElement $element)
     {
-        $this->expression = $expression;
+        $this->fields = array_map([Escaper::class, 'escapeFieldName'], $fields);
+        $this->element = $element;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
-        $withParentheses = $this->expression->includeSpace() || ctype_digit(substr($this->expression->render(), 0, 1));
-
         return sprintf(
-            true === $withParentheses ? self::WITH_SPACE_PATTERN : self::WITHOUT_SPACE_PATTERN,
-            $this->expression->render()
+            '@%s:%s',
+            (OrGroup::fromStrings(...$this->fields)->render()),
+            EncapsulationGroup::simple($this->element)->render()
         );
     }
 
-    public function includeSpace(): bool
+    public function priority(): int
     {
-        return false;
+        return self::PRIORITY_NORMAL;
     }
 
-    public function priority(): int
+    public function includeSpace(): bool
     {
-        return $this->expression->priority();
+        return false;
     }
 }
diff --git a/src/Search/QueryBuilder/FuzzyWord.php b/src/Query/Builder/Fuzzy.php
similarity index 61%
rename from src/Search/QueryBuilder/FuzzyWord.php
rename to src/Query/Builder/Fuzzy.php
index b62726e..ffce965 100644
--- a/src/Search/QueryBuilder/FuzzyWord.php
+++ b/src/Query/Builder/Fuzzy.php
@@ -19,42 +19,41 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use MacFJA\RediSearch\Search\Exception\OutOfRangeLevenshteinDistanceException;
-use function sprintf;
-use function str_repeat;
+use MacFJA\RediSearch\Exception\OutOfRangeLevenshteinDistanceException;
+use MacFJA\RediSearch\Query\Escaper;
 
-class FuzzyWord implements PartialQuery
+class Fuzzy implements QueryElement
 {
     /** @var string */
     private $word;
-
     /** @var int */
-    private $levenshteinDistance;
+    private $distance;
 
-    public function __construct(string $word, int $levenshteinDistance = 1)
+    public function __construct(string $word, int $distance = 1)
     {
-        if ($levenshteinDistance < 1 || $levenshteinDistance > 3) {
-            throw new OutOfRangeLevenshteinDistanceException($levenshteinDistance);
+        if ($distance < 1 || $distance > 3) {
+            throw new OutOfRangeLevenshteinDistanceException($distance);
         }
-        $this->word = $word;
-        $this->levenshteinDistance = $levenshteinDistance;
+        $this->word = trim($word);
+        $this->distance = $distance;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
-        return sprintf('%1$s%2$s%1$s', str_repeat('%', $this->levenshteinDistance), EscapeHelper::escapeFuzzy($this->word));
+        $fuzzy = str_repeat('%', $this->distance);
+
+        return $fuzzy.Escaper::escapeFuzzy($this->word).$fuzzy;
     }
 
-    public function includeSpace(): bool
+    public function priority(): int
     {
-        return false;
+        return self::PRIORITY_NORMAL;
     }
 
-    public function priority(): int
+    public function includeSpace(): bool
     {
-        return self::PRIORITY_BEFORE;
+        return strpos($this->word, ' ') > 0;
     }
 }
diff --git a/src/Query/Builder/GeoFacet.php b/src/Query/Builder/GeoFacet.php
new file mode 100644
index 0000000..58e39b4
--- /dev/null
+++ b/src/Query/Builder/GeoFacet.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+use MacFJA\RediSearch\Exception\UnknownUnitException;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+use Respect\Validation\Rules\In;
+
+class GeoFacet extends FieldFacet
+{
+    /**
+     * @param array<string> $fields
+     */
+    public function __construct(array $fields, float $lon, float $lat, float $radius, string $unit)
+    {
+        if (!(new In(GeoFilterOption::UNITS))->validate($unit)) {
+            throw new UnknownUnitException($unit);
+        }
+
+        $group = new EncapsulationGroup('[', ']', new RawElement(sprintf(
+            '%f %f %f %s',
+            $lon,
+            $lat,
+            $radius,
+            $unit
+        )));
+        parent::__construct($fields, $group);
+    }
+}
diff --git a/src/Query/Builder/Negation.php b/src/Query/Builder/Negation.php
new file mode 100644
index 0000000..6397441
--- /dev/null
+++ b/src/Query/Builder/Negation.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+use MacFJA\RediSearch\Query\Escaper;
+
+class Negation implements QueryElement, QueryElementDecorator
+{
+    /** @var QueryElement */
+    private $element;
+
+    public function __construct(QueryElement $element)
+    {
+        $this->element = $element;
+    }
+
+    public function render(?callable $escaper = null): string
+    {
+        return '-'.EncapsulationGroup::simple($this->element)->render([Escaper::class, 'escapeNegation']);
+    }
+
+    public function priority(): int
+    {
+        return self::PRIORITY_NORMAL;
+    }
+
+    public function includeSpace(): bool
+    {
+        return false;
+    }
+}
diff --git a/src/Query/Builder/NumericFacet.php b/src/Query/Builder/NumericFacet.php
new file mode 100644
index 0000000..50f2302
--- /dev/null
+++ b/src/Query/Builder/NumericFacet.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+use function is_array;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\NumericRangeTrait;
+
+class NumericFacet extends FieldFacet
+{
+    use NumericRangeTrait;
+
+    /**
+     * @param array<string>|string $fields
+     */
+    public function __construct($fields, ?float $min, ?float $max, bool $isMinInclusive = true, bool $isMaxInclusive = true)
+    {
+        if (!is_array($fields)) {
+            $fields = [$fields];
+        }
+        $bounds = self::renderBound($min, $max, $isMinInclusive, $isMaxInclusive);
+        $group = new EncapsulationGroup('[', ']', AndGroup::fromStrings(...$bounds));
+        parent::__construct($fields, $group);
+    }
+}
diff --git a/alias/Words/SpellingResult.php b/src/Query/Builder/Optional.php
similarity index 67%
rename from alias/Words/SpellingResult.php
rename to src/Query/Builder/Optional.php
index d537be2..7342e87 100644
--- a/alias/Words/SpellingResult.php
+++ b/src/Query/Builder/Optional.php
@@ -19,9 +19,32 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RedisSearch\Words;
+namespace MacFJA\RediSearch\Query\Builder;
 
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Words\SpellingResult */
-final class SpellingResult extends \MacFJA\RediSearch\Words\SpellingResult
+use MacFJA\RediSearch\Query\Escaper;
+
+class Optional implements QueryElement, QueryElementDecorator
 {
+    /** @var QueryElement */
+    private $element;
+
+    public function __construct(QueryElement $element)
+    {
+        $this->element = $element;
+    }
+
+    public function render(?callable $escaper = null): string
+    {
+        return '~'.EncapsulationGroup::simple($this->element)->render([Escaper::class, 'escapeOptional']);
+    }
+
+    public function priority(): int
+    {
+        return self::PRIORITY_NORMAL;
+    }
+
+    public function includeSpace(): bool
+    {
+        return false;
+    }
 }
diff --git a/src/Index/Builder/GeoField.php b/src/Query/Builder/OrGroup.php
similarity index 77%
rename from src/Index/Builder/GeoField.php
rename to src/Query/Builder/OrGroup.php
index 50ff85e..f0933f1 100644
--- a/src/Index/Builder/GeoField.php
+++ b/src/Query/Builder/OrGroup.php
@@ -19,17 +19,14 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Builder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-class GeoField extends AbstractField
+class OrGroup extends AbstractGroup
 {
-    public function __construct(string $name, bool $noIndex = false)
+    public function render(?callable $escaper = null): string
     {
-        parent::__construct($name, false, $noIndex);
-    }
-
-    public function getType(): string
-    {
-        return self::TYPE_GEO;
+        return implode('|', array_map(static function (QueryElement $element) use ($escaper): string {
+            return $element->render($escaper);
+        }, $this->elements));
     }
 }
diff --git a/src/Search/QueryBuilder/Optional.php b/src/Query/Builder/Prefix.php
similarity index 67%
rename from src/Search/QueryBuilder/Optional.php
rename to src/Query/Builder/Prefix.php
index d4144a4..6648dc1 100644
--- a/src/Search/QueryBuilder/Optional.php
+++ b/src/Query/Builder/Prefix.php
@@ -19,39 +19,37 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use function sprintf;
+use function call_user_func;
+use function is_callable;
 
-class Optional implements PartialQuery
+class Prefix implements QueryElement
 {
-    private const WITH_SPACE_PATTERN = '~(%s)';
+    /** @var string */
+    private $prefix;
 
-    private const WITHOUT_SPACE_PATTERN = '~%s';
-
-    /** @var PartialQuery */
-    private $expression;
-
-    public function __construct(PartialQuery $expression)
+    public function __construct(string $prefix)
     {
-        $this->expression = $expression;
+        $this->prefix = $prefix;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
-        return sprintf(
-            $this->expression->includeSpace() ? self::WITH_SPACE_PATTERN : self::WITHOUT_SPACE_PATTERN,
-            $this->expression->render()
-        );
+        if (is_callable($escaper)) {
+            return call_user_func($escaper, $this->prefix).'*';
+        }
+
+        return $this->prefix.'*';
     }
 
-    public function includeSpace(): bool
+    public function priority(): int
     {
-        return false;
+        return self::PRIORITY_NORMAL;
     }
 
-    public function priority(): int
+    public function includeSpace(): bool
     {
-        return $this->expression->priority();
+        return false;
     }
 }
diff --git a/src/Search/QueryBuilder/PartialQuery.php b/src/Query/Builder/QueryElement.php
similarity index 88%
rename from src/Search/QueryBuilder/PartialQuery.php
rename to src/Query/Builder/QueryElement.php
index 3c5cf91..46ded20 100644
--- a/src/Search/QueryBuilder/PartialQuery.php
+++ b/src/Query/Builder/QueryElement.php
@@ -19,17 +19,17 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-interface PartialQuery
+interface QueryElement
 {
     public const PRIORITY_BEFORE = 0;
-
     public const PRIORITY_NORMAL = 1;
+    public const PRIORITY_AFTER = 2;
 
-    public function render(): string;
-
-    public function includeSpace(): bool;
+    public function render(?callable $escaper = null): string;
 
     public function priority(): int;
+
+    public function includeSpace(): bool;
 }
diff --git a/src/Search/QueryBuilder/QueryExpressionAttribute.php b/src/Query/Builder/QueryElementAttribute.php
similarity index 77%
rename from src/Search/QueryBuilder/QueryExpressionAttribute.php
rename to src/Query/Builder/QueryElementAttribute.php
index c796655..c1a3f9c 100644
--- a/src/Search/QueryBuilder/QueryExpressionAttribute.php
+++ b/src/Query/Builder/QueryElementAttribute.php
@@ -19,22 +19,19 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
+use function count;
 use function implode;
 use function is_bool;
 use function is_float;
 use function is_int;
 use function sprintf;
 
-class QueryExpressionAttribute implements PartialQuery
+class QueryElementAttribute implements QueryElement, QueryElementDecorator
 {
-    private const WITH_SPACE_PATTERN = '(%s) => { %s }';
-
-    private const WITHOUT_SPACE_PATTERN = '%s => { %s }';
-
-    /** @var PartialQuery */
-    private $expression;
+    /** @var QueryElement */
+    private $element;
 
     /** @var null|float */
     private $weight;
@@ -48,16 +45,16 @@ class QueryExpressionAttribute implements PartialQuery
     /** @var null|bool */
     private $phonetic;
 
-    public function __construct(PartialQuery $expression, ?float $weight = null, ?int $slop = null, ?bool $inOrder = null, ?bool $phonetic = null)
+    public function __construct(QueryElement $element, ?float $weight = null, ?int $slop = null, ?bool $inOrder = null, ?bool $phonetic = null)
     {
-        $this->expression = $expression;
+        $this->element = $element;
         $this->weight = $weight;
         $this->slop = $slop;
         $this->inOrder = $inOrder;
         $this->phonetic = $phonetic;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
         $attributes = [];
         if (is_float($this->weight)) {
@@ -72,11 +69,16 @@ public function render(): string
         if (is_bool($this->phonetic)) {
             $attributes[] = sprintf('$phonetic: %s;', $this->phonetic ? 'true' : 'false');
         }
+
+        if (0 === count($attributes)) {
+            return $this->element->render($escaper);
+        }
+
         $attributesString = implode(' ', $attributes);
 
         return sprintf(
-            $this->expression->includeSpace() ? self::WITH_SPACE_PATTERN : self::WITHOUT_SPACE_PATTERN,
-            $this->expression->render(),
+            '%s => { %s }',
+            EncapsulationGroup::simple($this->element)->render($escaper),
             $attributesString
         );
     }
@@ -88,6 +90,6 @@ public function includeSpace(): bool
 
     public function priority(): int
     {
-        return $this->expression->priority();
+        return $this->element->priority();
     }
 }
diff --git a/src/Search/Exception/SyntaxErrorException.php b/src/Query/Builder/QueryElementDecorator.php
similarity index 90%
rename from src/Search/Exception/SyntaxErrorException.php
rename to src/Query/Builder/QueryElementDecorator.php
index 938c70c..4208ad9 100644
--- a/src/Search/Exception/SyntaxErrorException.php
+++ b/src/Query/Builder/QueryElementDecorator.php
@@ -19,10 +19,8 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\Exception;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use RuntimeException;
-
-class SyntaxErrorException extends RuntimeException
+interface QueryElementDecorator extends QueryElement
 {
 }
diff --git a/alias/Index.php b/src/Query/Builder/QueryElementGroup.php
similarity index 88%
rename from alias/Index.php
rename to src/Query/Builder/QueryElementGroup.php
index b0551cc..7f8e3aa 100644
--- a/alias/Index.php
+++ b/src/Query/Builder/QueryElementGroup.php
@@ -19,9 +19,9 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RedisSearch;
+namespace MacFJA\RediSearch\Query\Builder;
 
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Index */
-final class Index extends \MacFJA\RediSearch\Index
+interface QueryElementGroup extends QueryElement
 {
+    public function addElement(QueryElement $element): self;
 }
diff --git a/src/Search/QueryBuilder/RawExpression.php b/src/Query/Builder/RawElement.php
similarity index 90%
rename from src/Search/QueryBuilder/RawExpression.php
rename to src/Query/Builder/RawElement.php
index 895f016..e1e6d11 100644
--- a/src/Search/QueryBuilder/RawExpression.php
+++ b/src/Query/Builder/RawElement.php
@@ -19,9 +19,9 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-class RawExpression implements PartialQuery
+class RawElement implements QueryElement
 {
     /** @var string */
     private $content;
@@ -31,7 +31,7 @@ public function __construct(string $content)
         $this->content = $content;
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
         return $this->content;
     }
diff --git a/src/Query/Builder/TagFacet.php b/src/Query/Builder/TagFacet.php
new file mode 100644
index 0000000..a9cac96
--- /dev/null
+++ b/src/Query/Builder/TagFacet.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+class TagFacet extends FieldFacet
+{
+    public function __construct(array $fields, string ...$values)
+    {
+        $orGroup = new OrGroup(array_map(static function (string $value): QueryElement {
+            return new Word($value);
+        }, $values));
+
+        $group = new EncapsulationGroup('{', '}', $orGroup);
+        parent::__construct($fields, $group);
+    }
+}
diff --git a/src/Query/Builder/TextFacet.php b/src/Query/Builder/TextFacet.php
new file mode 100644
index 0000000..144d89c
--- /dev/null
+++ b/src/Query/Builder/TextFacet.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+use function count;
+
+class TextFacet extends FieldFacet
+{
+    /**
+     * @param array<string> $fields
+     */
+    public function __construct(array $fields, string ...$values)
+    {
+        $orGroup = new OrGroup(array_map(static function (string $value): QueryElement {
+            return new ExactMatch($value);
+        }, $values));
+
+        $group = new EncapsulationGroup('(', ')', $orGroup, count($values) < 2);
+        parent::__construct($fields, $group);
+    }
+}
diff --git a/src/Search/QueryBuilder/Word.php b/src/Query/Builder/Word.php
similarity index 70%
rename from src/Search/QueryBuilder/Word.php
rename to src/Query/Builder/Word.php
index eb5b710..add431d 100644
--- a/src/Search/QueryBuilder/Word.php
+++ b/src/Query/Builder/Word.php
@@ -19,37 +19,37 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Query\Builder;
 
-use function count;
-use function implode;
-use MacFJA\RediSearch\Helper\EscapeHelper;
+use function call_user_func;
+use function is_callable;
 
-class Word implements PartialQuery
+class Word implements QueryElement
 {
-    /** @var array<string> */
-    private $words = [];
+    /** @var string */
+    private $word;
 
-    /**
-     * @param string ...$words
-     */
-    public function __construct(string ...$words)
+    public function __construct(string $word)
     {
-        $this->words = $words;
+        $this->word = trim($word);
     }
 
-    public function render(): string
+    public function render(?callable $escaper = null): string
     {
-        return EscapeHelper::escapeWord(implode(' ', $this->words));
+        if (is_callable($escaper)) {
+            return call_user_func($escaper, $this->word);
+        }
+
+        return $this->word;
     }
 
-    public function includeSpace(): bool
+    public function priority(): int
     {
-        return count($this->words) > 1;
+        return self::PRIORITY_NORMAL;
     }
 
-    public function priority(): int
+    public function includeSpace(): bool
     {
-        return self::PRIORITY_BEFORE;
+        return strpos($this->word, ' ') > 0;
     }
 }
diff --git a/src/Helper/EscapeHelper.php b/src/Query/Escaper.php
similarity index 90%
rename from src/Helper/EscapeHelper.php
rename to src/Query/Escaper.php
index 705de16..dc1a7cd 100644
--- a/src/Helper/EscapeHelper.php
+++ b/src/Query/Escaper.php
@@ -19,14 +19,14 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Helper;
+namespace MacFJA\RediSearch\Query;
 
 use function preg_quote;
 use function preg_replace;
 use function sprintf;
 use function str_split;
 
-class EscapeHelper
+class Escaper
 {
     private const NOT_ESCAPED_CHAR_REGEX_PATTERN = '/(?<!\\\\)((?:\\\\\\\\)*)(%s)/';
 
@@ -59,7 +59,7 @@ public static function escapeNegation(string $text): string
         $text = self::escapeCommon($text);
         $text = self::escapeCharAtWordBegin($text, '-');
 
-        for ($number = 0; $number < 10; $number++) {
+        for ($number = 0; $number < 10; ++$number) {
             $text = self::escapeCharAtWordBegin($text, (string) $number);
         }
 
@@ -71,16 +71,21 @@ public static function escapeOptional(string $text): string
         return self::escapeCommon($text);
     }
 
+    public static function escapeNothing(string $text): string
+    {
+        return $text;
+    }
+
     /**
      * @codeCoverageIgnore
      */
     private static function escapeCharAtWordBegin(string $inText, string $char): string
     {
         return preg_replace(
-                sprintf(self::CHAR_AT_WORD_BEGIN_REGEX_PATTERN, preg_quote($char, '/')),
-                '$1\\\\$2',
-                $inText
-            ) ?? $inText;
+            sprintf(self::CHAR_AT_WORD_BEGIN_REGEX_PATTERN, preg_quote($char, '/')),
+            '$1\\\\$2',
+            $inText
+        ) ?? $inText;
     }
 
     /**
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
new file mode 100644
index 0000000..0b7c3de
--- /dev/null
+++ b/src/Redis/Command/AbstractCommand.php
@@ -0,0 +1,184 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use function count;
+use InvalidArgumentException;
+use function is_array;
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
+use Predis\Command\Command;
+
+/**
+ * @SuppressWarnings(PHPMD.NumberOfChildren)
+ */
+abstract class AbstractCommand extends Command
+{
+    /**
+     * @var array<array<CommandOption>|CommandOption|mixed>
+     */
+    protected $options;
+    /** @var string */
+    private $rediSearchVersion;
+    /** @var bool */
+    private $rebuild = true;
+
+    /**
+     * @param array<array<CommandOption>|CommandOption|mixed> $options
+     */
+    public function __construct(array $options, string $rediSearchVersion = '2.0.0')
+    {
+        $this->rediSearchVersion = $rediSearchVersion;
+
+        $this->options = $options;
+    }
+
+    public function getRediSearchVersion(): string
+    {
+        return $this->rediSearchVersion;
+    }
+
+    public function setRediSearchVersion(string $rediSearchVersion): self
+    {
+        $this->rediSearchVersion = $rediSearchVersion;
+
+        return $this;
+    }
+
+    /**
+     * @codeCoverageIgnore
+     *
+     * @param mixed $index
+     */
+    public function getArgument($index)
+    {
+        if ($this->rebuild) {
+            $this->buildArguments();
+        }
+
+        return parent::getArgument($index);
+    }
+
+    /**
+     * @return array<mixed>
+     */
+    public function getArguments()
+    {
+        if ($this->rebuild) {
+            $this->buildArguments();
+        }
+
+        return parent::getArguments();
+    }
+
+    /**
+     * @param array<mixed> $arguments
+     */
+    public function setArguments(array $arguments): void
+    {
+        $this->rebuild = false;
+        parent::setArguments($arguments);
+    }
+
+    /**
+     * @param array<mixed> $arguments
+     */
+    public function setRawArguments(array $arguments): void
+    {
+        $this->rebuild = true;
+        parent::setRawArguments($arguments);
+    }
+
+    /**
+     * @param array<mixed>|float|int|mixed|string $data
+     *
+     * @return mixed|string
+     */
+    public function parseResponse($data)
+    {
+        if ($this->rebuild) {
+            return $this->transformParsedResponse(parent::parseResponse($data));
+        }
+
+        return parent::parseResponse($data);
+    }
+
+    /**
+     * @return array<string>
+     */
+    abstract protected function getRequiredOptions(): array;
+
+    /**
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    protected function transformParsedResponse($data)
+    {
+        return $data;
+    }
+
+    private function buildArguments(): void
+    {
+        if (count($missing = $this->validateRequirements()) > 0) {
+            throw new InvalidArgumentException('Missing command option: '.implode(', ', $missing));
+        }
+
+        $options = array_reduce($this->options, static function ($flatten, $item) {
+            if (!is_array($item)) {
+                $item = [$item];
+            }
+
+            return array_merge($flatten, $item);
+        }, []);
+        $arguments = array_filter($options, function (CommandOption $option) {
+            return $option->isCompatible($this->rediSearchVersion) && $option->isValid();
+        });
+
+        $arguments = array_reduce($arguments, static function ($carry, CommandOption $option) {
+            return array_merge($carry, $option->render());
+        }, []);
+        $this->setRawArguments($arguments);
+    }
+
+    /**
+     * @return array<string>
+     */
+    private function validateRequirements(): array
+    {
+        $arguments = array_filter($this->options, function ($option) {
+            if ($option instanceof CommandOption) {
+                return $option->isCompatible($this->rediSearchVersion) && $option->isValid();
+            }
+            if (is_array($option)) {
+                $option = array_filter($option, function (CommandOption $option) {
+                    return $option->isCompatible($this->rediSearchVersion) && $option->isValid();
+                });
+
+                return count($option) > 0;
+            }
+
+            return false;
+        });
+
+        return array_diff($this->getRequiredOptions(), array_keys($arguments));
+    }
+}
diff --git a/src/Redis/Command/AddFieldOptionTrait.php b/src/Redis/Command/AddFieldOptionTrait.php
new file mode 100644
index 0000000..b2401f4
--- /dev/null
+++ b/src/Redis/Command/AddFieldOptionTrait.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use BadMethodCallException;
+use function get_class;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+
+trait AddFieldOptionTrait
+{
+    public function addTextField(string $name, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false): self
+    {
+        return $this->addField(
+            (new TextFieldOption())
+                ->setField($name)
+                ->setNoStem($noStem)
+                ->setWeight($weight)
+                ->setPhonetic($phonetic)
+                ->setSortable($sortable)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addNumericField(string $name, bool $sortable = false, bool $noIndex = false): self
+    {
+        return $this->addField(
+            (new NumericFieldOption())
+                ->setField($name)
+                ->setSortable($sortable)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addGeoField(string $name, bool $noIndex = false): self
+    {
+        return $this->addField(
+            (new GeoFieldOption())
+                ->setField($name)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addTagField(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false): self
+    {
+        return $this->addField(
+            (new TagFieldOption())
+                ->setField($name)
+                ->setSeparator($separator)
+                ->setSortable($sortable)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addField(CreateCommandFieldOption $option): self
+    {
+        if (!($this instanceof AbstractCommand)) {
+            throw new BadMethodCallException('This method is not callable in '.get_class($this));
+        }
+
+        $this->options['fields'][$option->getFieldName()] = $option;
+
+        return $this;
+    }
+}
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
new file mode 100644
index 0000000..fbaac26
--- /dev/null
+++ b/src/Redis/Command/Aggregate.php
@@ -0,0 +1,209 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use function assert;
+use function is_int;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\LimitOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+use MacFJA\RediSearch\Redis\Response\AggregateResponseItem;
+use MacFJA\RediSearch\Redis\Response\ArrayResponseTrait;
+use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
+
+/**
+ * @method CursorResponse|PaginatedResponse<AggregateResponseItem> parseResponse(mixed $data)
+ */
+class Aggregate extends AbstractCommand implements PaginatedCommand
+{
+    use ArrayResponseTrait;
+
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'query' => new NamelessOption(null, '>=2.0.0'),
+            'verbatim' => new FlagOption('VERBATIM', false, '>= 2.0.0'),
+            'load' => new NumberedOption('LOAD', null, '>=2.0.0'),
+            'groupby' => [],
+            'sortby' => new SortByOption(),
+            'apply' => [],
+            'limit' => new LimitOption(),
+            'filter' => [],
+            'cursor' => new WithCursor(),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function getIndex(): string
+    {
+        return $this->options['index']->getValue();
+    }
+
+    public function setQuery(string $query): self
+    {
+        $this->options['query']->setValue($query);
+
+        return $this;
+    }
+
+    public function setVerbatim(bool $active = true): self
+    {
+        $this->options['verbatim']->setActive($active);
+
+        return $this;
+    }
+
+    public function addSortBy(string $field, ?string $direction = null): self
+    {
+        $this->options['sortby']->addField($field, $direction);
+
+        return $this;
+    }
+
+    public function setSortByMax(int $max): self
+    {
+        $this->options['sortby']->setMax($max);
+
+        return $this;
+    }
+
+    /**
+     * @return $this
+     */
+    public function setLimit(int $offset, int $size): PaginatedCommand
+    {
+        $this->options['limit']->setOffset($offset)->setSize($size);
+
+        return $this;
+    }
+
+    public function addFilter(string $expression): self
+    {
+        $this->options['filter'][] = new NamedOption('FILTER', $expression);
+
+        return $this;
+    }
+
+    public function addApply(string $expression, string $alias): self
+    {
+        $apply = new ApplyOption();
+        $apply
+            ->setDataOfOption('expression', $expression)
+            ->setDataOfOption('alias', $alias)
+        ;
+        $this->options['apply'][] = $apply;
+
+        return $this;
+    }
+
+    public function addGroupBy(GroupByOption $group): self
+    {
+        $this->options['groupby'][] = $group;
+
+        return $this;
+    }
+
+    public function setLoad(string ...$field): self
+    {
+        $this->options['load']->setArguments($field);
+
+        return $this;
+    }
+
+    public function setWithCursor(?int $count = null, ?int $maxIdle = null): self
+    {
+        $this->options['cursor']
+            ->setDataOfOption('enabled', true)
+            ->setDataOfOption('count', $count)
+            ->setDataOfOption('maxidle', $maxIdle)
+        ;
+
+        return $this;
+    }
+
+    public function setWithoutCursor(): self
+    {
+        $this->options['cursor']->setDataOfOption('enabled', false);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.AGGREGATE';
+    }
+
+    public function getOffset(): ?int
+    {
+        /** @var LimitOption $limit */
+        $limit = $this->options['limit'];
+
+        return $limit->getOffset();
+    }
+
+    public function getSize(): ?int
+    {
+        /** @var LimitOption $limit */
+        $limit = $this->options['limit'];
+
+        return $limit->getSize();
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'query'];
+    }
+
+    protected function transformParsedResponse($data)
+    {
+        if (true === $this->options['cursor']->getDataOfOption('enabled')) {
+            return CursorRead::transformResponse(
+                $data,
+                $this->options['cursor']->getDataOfOption('count'),
+                $this->getIndex(),
+                $this->getRediSearchVersion()
+            );
+        }
+
+        $totalCount = array_shift($data);
+        assert(is_int($totalCount));
+
+        $items = array_map(static function (array $document) {
+            return new AggregateResponseItem(self::getPairs($document));
+        }, $data);
+
+        return new PaginatedResponse($this, $totalCount, $items);
+    }
+}
diff --git a/src/Aggregate/Exception/NotEnoughPropertiesException.php b/src/Redis/Command/AggregateCommand/ApplyOption.php
similarity index 71%
rename from src/Aggregate/Exception/NotEnoughPropertiesException.php
rename to src/Redis/Command/AggregateCommand/ApplyOption.php
index 6e40ef2..384a2f0 100644
--- a/src/Aggregate/Exception/NotEnoughPropertiesException.php
+++ b/src/Redis/Command/AggregateCommand/ApplyOption.php
@@ -19,20 +19,18 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Aggregate\Exception;
+namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
 
-use OutOfRangeException;
-use Throwable;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 
-/**
- * @codeCoverageIgnore
- *
- * @deprecated This exception is never used.
- */
-class NotEnoughPropertiesException extends OutOfRangeException
+class ApplyOption extends GroupedOption
 {
-    public function __construct(string $message = 'You must have at least one property', int $code = 0, ?Throwable $previous = null)
+    public function __construct()
     {
-        parent::__construct($message, $code, $previous);
+        parent::__construct([
+            'expression' => new NamedOption('APPLY', null, '>=2.0.0'),
+            'alias' => new NamedOption('AS', null, '>=2.0.0'),
+        ], ['expression'], [], '>=2.0.0');
     }
 }
diff --git a/src/Redis/Command/AggregateCommand/GroupByOption.php b/src/Redis/Command/AggregateCommand/GroupByOption.php
new file mode 100644
index 0000000..23ca64e
--- /dev/null
+++ b/src/Redis/Command/AggregateCommand/GroupByOption.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
+
+use function count;
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+use MacFJA\RediSearch\Redis\Command\PrefixFieldName;
+
+class GroupByOption extends AbstractCommandOption
+{
+    use PrefixFieldName;
+
+    /** @var array<ReduceOption> */
+    private $reducers = [];
+    /** @var array<string> */
+    private $fields;
+
+    /**
+     * @param array<string>       $fields
+     * @param array<ReduceOption> $reducers
+     */
+    public function __construct(array $fields, array $reducers = [])
+    {
+        $this->reducers = $reducers;
+        $this->fields = $fields;
+        parent::__construct('>=2.0.0');
+    }
+
+    public function addReducer(ReduceOption $reducer): self
+    {
+        $this->reducers[] = $reducer;
+
+        return $this;
+    }
+
+    public function isValid(): bool
+    {
+        return true;
+    }
+
+    /**
+     * @return array<string,mixed>
+     */
+    public function getOptionData()
+    {
+        return [
+            'fields' => $this->fields,
+            'reducers' => $this->reducers,
+        ];
+    }
+
+    protected function doRender(?string $version): array
+    {
+        $groupBy = array_merge(['GROUPBY', count($this->fields)], self::prefixFields($this->fields));
+        $reducers = array_reduce($this->reducers, static function ($flatten, ReduceOption $reduce) use ($version) {
+            return array_merge($flatten, $reduce->render($version));
+        }, []);
+
+        return array_merge($groupBy, $reducers);
+    }
+}
diff --git a/src/Index/Builder/Exception/MissingNameException.php b/src/Redis/Command/AggregateCommand/LimitOption.php
similarity index 88%
rename from src/Index/Builder/Exception/MissingNameException.php
rename to src/Redis/Command/AggregateCommand/LimitOption.php
index 014ba4b..7412a6f 100644
--- a/src/Index/Builder/Exception/MissingNameException.php
+++ b/src/Redis/Command/AggregateCommand/LimitOption.php
@@ -19,10 +19,8 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Builder\Exception;
+namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
 
-use InvalidArgumentException;
-
-class MissingNameException extends InvalidArgumentException
+class LimitOption extends \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
 {
 }
diff --git a/src/Aggregate/Reducer.php b/src/Redis/Command/AggregateCommand/ReduceOption.php
similarity index 61%
rename from src/Aggregate/Reducer.php
rename to src/Redis/Command/AggregateCommand/ReduceOption.php
index 1dda170..070a27b 100644
--- a/src/Aggregate/Reducer.php
+++ b/src/Redis/Command/AggregateCommand/ReduceOption.php
@@ -19,20 +19,21 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Aggregate;
+namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
 
 use function count;
 use function is_bool;
 use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\PartialQuery;
-use Throwable;
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+use MacFJA\RediSearch\Redis\Command\PrefixFieldName;
 
 /**
  * @SuppressWarnings(PHPMD.TooManyPublicMethods)
  */
-class Reducer implements PartialQuery
+class ReduceOption extends AbstractCommandOption
 {
+    use PrefixFieldName;
+
     /** @var string */
     private $function;
 
@@ -44,89 +45,90 @@ class Reducer implements PartialQuery
 
     /**
      * @param array<float|int|string> $arguments
-     *
-     * @throws Throwable
      */
-    public function __construct(string $function, array $arguments = [], ?string $name = null)
+    public function __construct(string $function, array $arguments = [], ?string $name = null, ?string $versionConstraint = null)
     {
-        DataHelper::assertArrayOf($arguments, 'scalar');
         $this->function = $function;
         $this->arguments = $arguments;
         $this->name = $name;
+        parent::__construct($versionConstraint);
     }
 
-    public function getQueryParts(): array
+    public function isValid(): bool
     {
-        $query = ['REDUCE', $this->function, count($this->arguments)];
-        foreach ($this->arguments as $argument) {
-            $query[] = $argument;
-        }
-        if (is_string($this->name)) {
-            $query[] = 'AS';
-            $query[] = $this->name;
-        }
+        return true;
+    }
 
-        return $query;
+    /**
+     * @return array<string,null|array|string>
+     */
+    public function getOptionData()
+    {
+        return [
+            'function' => $this->function,
+            'arguments' => $this->arguments,
+            'alias' => $this->name,
+        ];
     }
 
     public static function count(?string $name = null): self
     {
-        return new self('COUNT', [], $name);
+        return new self('COUNT', [], $name, '>=2.0.0');
     }
 
     public static function countDistinct(string $property, ?string $name = null): self
     {
-        return new self('COUNT_DISTINCT', ['@'.$property], $name);
+        return new self('COUNT_DISTINCT', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function countDistinctish(string $property, ?string $name = null): self
     {
-        return new self('COUNT_DISTINCTISH', ['@'.$property], $name);
+        return new self('COUNT_DISTINCTISH', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function sum(string $property, ?string $name = null): self
     {
-        return new self('SUM', ['@'.$property], $name);
+        return new self('SUM', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function minimum(string $property, ?string $name = null): self
     {
-        return new self('MIN', ['@'.$property], $name);
+        return new self('MIN', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function maximum(string $property, ?string $name = null): self
     {
-        return new self('MAX', ['@'.$property], $name);
+        return new self('MAX', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function average(string $property, ?string $name = null): self
     {
-        return new self('AVG', ['@'.$property], $name);
+        return new self('AVG', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function standardDeviation(string $property, ?string $name = null): self
     {
-        return new self('STDDEV', ['@'.$property], $name);
+        return new self('STDDEV', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function quantile(string $property, float $quantile, ?string $name = null): self
     {
-        return new self('QUANTILE', ['@'.$property, $quantile], $name);
+        return new self('QUANTILE', [self::prefixField($property), $quantile], $name, '>=2.0.0');
     }
 
     public static function median(string $property, ?string $name = null): self
     {
-        return new self('QUANTILE', ['@'.$property, 0.5], $name);
+        return new self('QUANTILE', [self::prefixField($property), 0.5], $name, '>=2.0.0');
     }
 
     public static function toList(string $property, ?string $name = null): self
     {
-        return new self('TOLIST', ['@'.$property], $name);
+        return new self('TOLIST', [self::prefixField($property)], $name, '>=2.0.0');
     }
 
     public static function firstValue(string $property, ?string $sortByProperty = null, ?bool $ascending = null, ?string $name = null): self
     {
-        $arguments = ['@'.$property];
+        $arguments = [self::prefixField($property)];
         if (is_string($sortByProperty)) {
             $arguments[] = 'BY';
             $arguments[] = '@'.$sortByProperty;
@@ -135,6 +137,13 @@ public static function firstValue(string $property, ?string $sortByProperty = nu
             }
         }
 
-        return new self('FIRST_VALUE', $arguments, $name);
+        return new self('FIRST_VALUE', $arguments, $name, '>=2.0.0');
+    }
+
+    protected function doRender(?string $version): array
+    {
+        $alias = is_string($this->name) ? ['AS', $this->name] : [];
+
+        return array_merge(['REDUCE', $this->function, count($this->arguments)], $this->arguments, $alias);
     }
 }
diff --git a/src/Redis/Command/AggregateCommand/SortByOption.php b/src/Redis/Command/AggregateCommand/SortByOption.php
new file mode 100644
index 0000000..66cc86a
--- /dev/null
+++ b/src/Redis/Command/AggregateCommand/SortByOption.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
+
+use function count;
+use function is_int;
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+use MacFJA\RediSearch\Redis\Command\PrefixFieldName;
+
+class SortByOption extends AbstractCommandOption
+{
+    use PrefixFieldName;
+    /** @var array<string,null|string> */
+    private $fields = [];
+    /** @var null|int */
+    private $max;
+
+    public function setMax(?int $max): SortByOption
+    {
+        $this->max = $max;
+
+        return $this;
+    }
+
+    public function addField(string $name, ?string $direction = null): void
+    {
+        $this->fields[self::prefixField($name)] = $direction;
+    }
+
+    public function isValid(): bool
+    {
+        return count($this->fields) > 0;
+    }
+
+    /**
+     * @return array<string,null|array<string,null|string>|int>
+     */
+    public function getOptionData()
+    {
+        return [
+            'fields' => $this->fields,
+            'max' => $this->max,
+        ];
+    }
+
+    protected function doRender(?string $version): array
+    {
+        $fields = [];
+        foreach ($this->fields as $name => $direction) {
+            $fields[] = $name;
+            if ('ASC' === $direction || 'DESC' === $direction) {
+                $fields[] = $direction;
+            }
+        }
+        $max = is_int($this->max) ? ['MAX', $this->max] : [];
+
+        return array_merge(['SORTBY', count($fields)], $fields, $max);
+    }
+}
diff --git a/src/Redis/Command/AggregateCommand/WithCursor.php b/src/Redis/Command/AggregateCommand/WithCursor.php
new file mode 100644
index 0000000..c523e53
--- /dev/null
+++ b/src/Redis/Command/AggregateCommand/WithCursor.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
+
+class WithCursor extends GroupedOption
+{
+    public function __construct()
+    {
+        parent::__construct([
+            'enabled' => new NotEmptyOption(new FlagOption('WITHCURSOR', false, '>=2.0.0')),
+            'count' => CV::isNumeric(new NamedOption('COUNT', null, '>=2.0.0')),
+            'maxidle' => CV::isNumeric(new NamedOption('MAXIDLE', null, '>=2.0.0')),
+        ], ['enabled'], [], '>=2.0.0');
+    }
+}
diff --git a/src/Redis/Command/AliasAdd.php b/src/Redis/Command/AliasAdd.php
new file mode 100644
index 0000000..bd0d78b
--- /dev/null
+++ b/src/Redis/Command/AliasAdd.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class AliasAdd extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'alias' => new NamelessOption(null, '>=2.0.0'),
+            'index' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setAlias(string $name): self
+    {
+        $this->options['alias']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.ALIASADD';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'alias'];
+    }
+}
diff --git a/src/Redis/Command/AliasDel.php b/src/Redis/Command/AliasDel.php
new file mode 100644
index 0000000..e16c9c1
--- /dev/null
+++ b/src/Redis/Command/AliasDel.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class AliasDel extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'alias' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setAlias(string $name): self
+    {
+        $this->options['alias']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.ALIASDEL';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['alias'];
+    }
+}
diff --git a/src/Redis/Command/AliasUpdate.php b/src/Redis/Command/AliasUpdate.php
new file mode 100644
index 0000000..ecb4baf
--- /dev/null
+++ b/src/Redis/Command/AliasUpdate.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class AliasUpdate extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'alias' => new NamelessOption(null, '>=2.0.0'),
+            'index' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setAlias(string $name): self
+    {
+        $this->options['alias']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.ALIASUPDATE';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'alias'];
+    }
+}
diff --git a/src/Redis/Command/Alter.php b/src/Redis/Command/Alter.php
new file mode 100644
index 0000000..4546a1e
--- /dev/null
+++ b/src/Redis/Command/Alter.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class Alter extends AbstractCommand
+{
+    use AddFieldOptionTrait;
+
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'schema' => new FlagOption('SCHEMA', true, '>=2.0.0'),
+            'add' => new FlagOption('ADD', true, '>=2.0.0'),
+            'fields' => [],
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.ALTER';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'schema', 'add', 'fields'];
+    }
+}
diff --git a/src/Index/Builder/TextField.php b/src/Redis/Command/Config.php
similarity index 51%
rename from src/Index/Builder/TextField.php
rename to src/Redis/Command/Config.php
index f10ac33..bc47740 100644
--- a/src/Index/Builder/TextField.php
+++ b/src/Redis/Command/Config.php
@@ -19,53 +19,47 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Builder;
+namespace MacFJA\RediSearch\Redis\Command;
 
-use MacFJA\RediSearch\Helper\RedisHelper;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 
-class TextField extends AbstractField
+/**
+ * @method array<string,string>|string parseResponse(mixed $data)
+ */
+class Config extends AbstractCommand
 {
-    /** @var bool */
-    private $noStem = false;
-
-    /** @var null|float */
-    private $weight;
-
-    /** @var null|string */
-    private $phonetic;
-
-    public function __construct(string $name, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false)
+    public function __construct(string $rediSearchVersion = '2.0.0')
     {
-        parent::__construct($name, $sortable, $noIndex);
-        $this->noStem = $noStem;
-        $this->weight = $weight;
-        $this->phonetic = $phonetic;
+        parent::__construct([
+            'action' => CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.0'), ['GET', 'HELP']),
+            'option' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
     }
 
-    public function isNoStem(): bool
+    public function setHelpMode(?string $optionName = null): self
     {
-        return $this->noStem;
-    }
+        $this->options['action']->setValue('HELP');
+        $this->options['option']->setValue($optionName ?? '*');
 
-    public function getWeight(): ?float
-    {
-        return $this->weight;
+        return $this;
     }
 
-    public function getPhonetic(): ?string
+    public function setGetMode(?string $optionName = null): self
     {
-        return $this->phonetic;
+        $this->options['action']->setValue('GET');
+        $this->options['option']->setValue($optionName ?? '*');
+
+        return $this;
     }
 
-    public function getType(): string
+    public function getId()
     {
-        return self::TYPE_TEXT;
+        return 'FT.CONFIG';
     }
 
-    protected function getAdditionalQueryParts(): array
+    protected function getRequiredOptions(): array
     {
-        $query = RedisHelper::buildQueryBoolean([], ['NOSTEM' => $this->noStem]);
-
-        return RedisHelper::buildQueryNotNull($query, ['WEIGHT' => $this->weight, 'PHONETIC' => $this->phonetic]);
+        return ['action', 'option'];
     }
 }
diff --git a/src/Redis/Command/ConfigSet.php b/src/Redis/Command/ConfigSet.php
new file mode 100644
index 0000000..db1ffca
--- /dev/null
+++ b/src/Redis/Command/ConfigSet.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class ConfigSet extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'action' => new FlagOption('SET', true, '>=2.0.0'),
+            'options' => [
+                CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.0 <2.0.6'), ['NOGC', 'MAXEXPANSIONS', 'TIMEOUT', 'ON_TIMEOUT', 'MIN_PHONETIC_TERM_LEN']),
+                CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.6'), ['NOGC', 'MINPREFIX', 'MAXEXPANSIONS', 'MAXFILTEREXPANSION', 'TIMEOUT', 'ON_TIMEOUT', 'MIN_PHONETIC_TERM_LEN']),
+            ],
+            'value' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    /**
+     * @param mixed $value
+     *
+     * @return $this
+     */
+    public function setConfig(string $name, $value): self
+    {
+        $this->options['options'][0]->setValue($name);
+        $this->options['options'][1]->setValue($name);
+        $this->options['value']->setValue($value);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.CONFIG';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['action', 'options', 'value'];
+    }
+}
diff --git a/src/Redis/Command/Create.php b/src/Redis/Command/Create.php
new file mode 100644
index 0000000..a8d05a2
--- /dev/null
+++ b/src/Redis/Command/Create.php
@@ -0,0 +1,202 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class Create extends AbstractCommand
+{
+    use LanguageOptionTrait;
+    use AddFieldOptionTrait;
+
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'structure' => CV::allowedValues(new NamedOption('ON', null, '>=2.0.0'), ['HASH']),
+            'prefixes' => new NotEmptyOption(new NumberedOption('PREFIX', null, '>=2.0.0')),
+            'filter' => new NamedOption('FILTER', null, '>=2.0.0'),
+            'default_lang' => $this->getLanguageOptions(),
+            'lang_field' => new NamedOption('LANGUAGE_FIELD', null, '>=2.0.0'),
+            'default_score' => CV::isNumeric(new NamedOption('SCORE', null, '>=2.0.0')),
+            'score_field' => new NamedOption('SCORE_FIELD', null, '>=2.0.0'),
+            'payload_field' => new NamedOption('PAYLOAD_FIELD', null, '>=2.0.0'),
+            'max_text_fields' => new FlagOption('MAXTEXTFIELDS', false, '>=2.0.0'),
+            'temporary' => CV::isNumeric(new NamedOption('TEMPORARY', null, '>=2.0.0')),
+            'no_offsets' => new FlagOption('NOOFFSETS', false, '>=2.0.0'),
+            'no_hl' => new FlagOption('NOHL', false, '>=2.0.0'),
+            'no_fields' => new FlagOption('NOFIELDS', false, '>=2.0.0'),
+            'no_freqs' => new FlagOption('NOFREQS', false, '>=2.0.0'),
+            'skip_initial_scan' => new FlagOption('SKIPINITIALSCAN', false, '>=2.0.0'),
+            'stopwords' => new NumberedOption('STOPWORDS', null, '>=2.0.0'),
+            'schema' => new FlagOption('SCHEMA', true, '>=2.0.0'),
+            'fields' => [],
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setStructure(string $structureType = 'HASH'): self
+    {
+        $this->options['structure']->setValue($structureType);
+
+        return $this;
+    }
+
+    public function setPrefixes(string ...$prefix): self
+    {
+        $this->options['prefixes']->setArguments($prefix);
+
+        return $this;
+    }
+
+    public function setFilter(string $filter): self
+    {
+        $this->options['filter']->setValue($filter);
+
+        return $this;
+    }
+
+    public function setDefaultLanguage(string $language): self
+    {
+        array_walk($this->options['default_lang'], static function (CV $option) use ($language): void {
+            $option->setValue($language);
+        });
+
+        return $this;
+    }
+
+    public function setLanguageField(string $fieldName): self
+    {
+        $this->options['lang_field']->setValue($fieldName);
+
+        return $this;
+    }
+
+    public function setDefaultScore(float $score): self
+    {
+        $this->options['default_score']->setValue($score);
+
+        return $this;
+    }
+
+    public function setScoreField(string $fieldName): self
+    {
+        $this->options['score_field']->setValue($fieldName);
+
+        return $this;
+    }
+
+    public function setPayloadField(string $fieldName): self
+    {
+        $this->options['payload_field']->setValue($fieldName);
+
+        return $this;
+    }
+
+    public function setMaxTextFields(bool $active = true): self
+    {
+        $this->options['max_text_fields']->setActive($active);
+
+        return $this;
+    }
+
+    public function setTemporary(int $seconds): self
+    {
+        $this->options['temporary']->setValue($seconds);
+
+        return $this;
+    }
+
+    public function setNoOffsets(bool $active = true): self
+    {
+        $this->options['no_offsets']->setActive($active);
+
+        return $this;
+    }
+
+    public function setNoHighLight(bool $active = true): self
+    {
+        $this->options['no_hl']->setActive($active);
+
+        return $this;
+    }
+
+    public function setNoFields(bool $active = true): self
+    {
+        $this->options['no_fields']->setActive($active);
+
+        return $this;
+    }
+
+    public function setNoFrequencies(bool $active = true): self
+    {
+        $this->options['no_freqs']->setActive($active);
+
+        return $this;
+    }
+
+    public function setSkipInitialScan(bool $active = true): self
+    {
+        $this->options['skip_initial_scan']->setActive($active);
+
+        return $this;
+    }
+
+    public function setStopWords(string ...$stopWords): self
+    {
+        $this->options['stopwords']->setArguments($stopWords);
+
+        return $this;
+    }
+
+    public function setNoStopWords(): self
+    {
+        $this->options['stopwords']->setArguments([]);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.CREATE';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'schema', 'fields'];
+    }
+}
diff --git a/alias/Aggregate/GroupBy.php b/src/Redis/Command/CreateCommand/CreateCommandFieldOption.php
similarity index 83%
rename from alias/Aggregate/GroupBy.php
rename to src/Redis/Command/CreateCommand/CreateCommandFieldOption.php
index bba509b..cdf49af 100644
--- a/alias/Aggregate/GroupBy.php
+++ b/src/Redis/Command/CreateCommand/CreateCommandFieldOption.php
@@ -19,9 +19,11 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RedisSearch\Aggregate;
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
 
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\GroupBy */
-final class GroupBy extends \MacFJA\RediSearch\Aggregate\GroupBy
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
+
+interface CreateCommandFieldOption extends CommandOption
 {
+    public function getFieldName(): string;
 }
diff --git a/src/Redis/Command/CreateCommand/GeoFieldOption.php b/src/Redis/Command/CreateCommand/GeoFieldOption.php
new file mode 100644
index 0000000..073b8cc
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/GeoFieldOption.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+
+/**
+ * @method GeoFieldOption setField(string $name)
+ * @method GeoFieldOption setNoIndex(bool $active)
+ * @method GeoFieldOption setSortable(bool $active)
+ */
+class GeoFieldOption extends GroupedOption implements CreateCommandFieldOption
+{
+    use WithPublicGroupedSetterTrait;
+
+    public function __construct()
+    {
+        parent::__construct([
+            'field' => new NamelessOption(null, '>=2.0.0'),
+            'type' => new FlagOption('GEO', true, '>=2.0.0'),
+            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
+            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.10'),
+        ], ['field', 'type'], ['type'], '>=2.0.0');
+    }
+
+    public function getFieldName(): string
+    {
+        return $this->getDataOfOption('field');
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function publicSetter(): array
+    {
+        return ['field', 'no_index', 'sortable'];
+    }
+}
diff --git a/src/Redis/Command/CreateCommand/NumericFieldOption.php b/src/Redis/Command/CreateCommand/NumericFieldOption.php
new file mode 100644
index 0000000..d14c86a
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/NumericFieldOption.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+
+/**
+ * @method NumericFieldOption setField(string $name)
+ * @method NumericFieldOption setNoIndex(bool $active)
+ * @method NumericFieldOption setSortable(bool $active)
+ */
+class NumericFieldOption extends GroupedOption implements CreateCommandFieldOption
+{
+    use WithPublicGroupedSetterTrait;
+
+    public function __construct()
+    {
+        parent::__construct([
+            'field' => new NamelessOption(null, '>=2.0.0'),
+            'type' => new FlagOption('NUMERIC', true, '>=2.0.0'),
+            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
+            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
+        ], ['field', 'type'], ['type'], '>=2.0.0');
+    }
+
+    public function getFieldName(): string
+    {
+        return $this->getDataOfOption('field');
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function publicSetter(): array
+    {
+        return ['field', 'no_index', 'sortable'];
+    }
+}
diff --git a/src/Redis/Command/CreateCommand/TagFieldOption.php b/src/Redis/Command/CreateCommand/TagFieldOption.php
new file mode 100644
index 0000000..a77d9d8
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/TagFieldOption.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+use Respect\Validation\Rules\Length;
+
+/**
+ * @method TagFieldOption setField(string $name)
+ * @method TagFieldOption setNoIndex(bool $active)
+ * @method TagFieldOption setSortable(bool $active)
+ * @method TagFieldOption setSeparator(?string $separator)
+ */
+class TagFieldOption extends GroupedOption implements CreateCommandFieldOption
+{
+    use WithPublicGroupedSetterTrait;
+
+    public function __construct()
+    {
+        parent::__construct([
+            'field' => new NamelessOption(null, '>=2.0.0'),
+            'type' => new FlagOption('TAG', true, '>=2.0.0'),
+            'separator' => new CustomValidatorOption(new NamedOption('SEPARATOR', null, '>=2.0.0'), new Length(1, 1)),
+            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
+            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
+        ], ['field', 'type'], ['type'], '>=2.0.0');
+    }
+
+    public function getFieldName(): string
+    {
+        return $this->getDataOfOption('field');
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function publicSetter(): array
+    {
+        return ['field', 'separator', 'sortable', 'no_index'];
+    }
+}
diff --git a/src/Redis/Command/CreateCommand/TextFieldOption.php b/src/Redis/Command/CreateCommand/TextFieldOption.php
new file mode 100644
index 0000000..de6eb1f
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/TextFieldOption.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+
+/**
+ * @method TextFieldOption setField(string $name)
+ * @method TextFieldOption setNoIndex(bool $active)
+ * @method TextFieldOption setSortable(bool $active)
+ * @method TextFieldOption setNoStem(bool $active)
+ * @method TextFieldOption setWeight(?float $weight)
+ * @method TextFieldOption setPhonetic(?string $phonetic)
+ */
+class TextFieldOption extends GroupedOption implements CreateCommandFieldOption
+{
+    use WithPublicGroupedSetterTrait;
+
+    public function __construct()
+    {
+        parent::__construct([
+            'field' => new NamelessOption(null, '>=2.0.0'),
+            'type' => new FlagOption('TEXT', true, '>=2.0.0'),
+            'no_stem' => new FlagOption('NOSTEM', false, '>=2.0.0'),
+            'weight' => CustomValidatorOption::isNumeric(new NamedOption('WEIGHT', null, '>=2.0.0')),
+            'phonetic' => CustomValidatorOption::allowedValues(new NamedOption('PHONETIC', null, '>=2.0.0'), ['dm:en', 'dm:fr', 'dm:pt', 'dm:es']),
+            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
+            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
+        ], ['field', 'type'], ['type'], '>=2.0.0');
+    }
+
+    public function getFieldName(): string
+    {
+        return $this->getDataOfOption('field');
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function publicSetter(): array
+    {
+        return ['field', 'no_stem', 'weight', 'phonetic', 'sortable', 'no_index'];
+    }
+}
diff --git a/src/Redis/Command/CursorDel.php b/src/Redis/Command/CursorDel.php
new file mode 100644
index 0000000..9bee9be
--- /dev/null
+++ b/src/Redis/Command/CursorDel.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class CursorDel extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'type' => new FlagOption('DEL', true, '>=2.0.0'),
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'cursor' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setCursorId(int $cursor): self
+    {
+        $this->options['cursor']->setValue($cursor);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.CURSOR';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['type', 'index', 'cursor'];
+    }
+}
diff --git a/src/Redis/Command/CursorRead.php b/src/Redis/Command/CursorRead.php
new file mode 100644
index 0000000..b037272
--- /dev/null
+++ b/src/Redis/Command/CursorRead.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use function assert;
+use function count;
+use function is_array;
+use function is_int;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Response\AggregateResponseItem;
+use MacFJA\RediSearch\Redis\Response\ArrayResponseTrait;
+use MacFJA\RediSearch\Redis\Response\CursorResponse;
+
+/**
+ * @method CursorResponse parseResponse(mixed $data)
+ */
+class CursorRead extends AbstractCommand
+{
+    use ArrayResponseTrait;
+
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'type' => new FlagOption('READ', true, '>=2.0.0'),
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'cursor' => new NamelessOption(null, '>=2.0.0'),
+            'count' => new NamedOption('COUNT', null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setCursorId(int $cursor): self
+    {
+        $this->options['cursor']->setValue($cursor);
+
+        return $this;
+    }
+
+    public function setCount(int $count): self
+    {
+        $this->options['count']->setValue($count);
+
+        return $this;
+    }
+
+    public function setNoCount(): self
+    {
+        $this->options['count']->setValue(null);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.CURSOR';
+    }
+
+    /**
+     * @param array<array<mixed>|int> $data
+     */
+    public static function transformResponse(array $data, ?int $count, string $index, string $rediSearchVersion = '2.0.0'): CursorResponse
+    {
+        [$rawResult, $cursorId] = $data;
+
+        assert(is_array($rawResult));
+        assert(is_int($cursorId));
+
+        $totalCount = array_shift($rawResult);
+        $items = array_map(static function (array $document) {
+            return new AggregateResponseItem(self::getPairs($document));
+        }, $rawResult);
+
+        $size = $count ?? count($items);
+
+        return new CursorResponse($cursorId, $totalCount, $items, $size, $index, $rediSearchVersion);
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['type', 'index', 'cursor'];
+    }
+
+    protected function transformParsedResponse($data)
+    {
+        return self::transformResponse(
+            $data,
+            $this->options['count']->getValue(),
+            $this->options['index']->getValue(),
+            $this->getRediSearchVersion()
+        );
+    }
+}
diff --git a/src/Redis/Command/DictAdd.php b/src/Redis/Command/DictAdd.php
new file mode 100644
index 0000000..c876a04
--- /dev/null
+++ b/src/Redis/Command/DictAdd.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method int parseResponse(mixed $data)
+ */
+class DictAdd extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dict' => new NamelessOption(null, '>=2.0.0'),
+            'terms' => [],
+        ], $rediSearchVersion);
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dict']->setValue($name);
+
+        return $this;
+    }
+
+    public function addTerm(string $term): self
+    {
+        $this->options['terms'][] = new NamelessOption($term);
+
+        return $this;
+    }
+
+    public function addTerms(string ...$term): self
+    {
+        array_walk($term, function (string $term): void {
+            $this->addTerm($term);
+        });
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.DICTADD';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dict', 'terms'];
+    }
+}
diff --git a/src/Redis/Command/DictDel.php b/src/Redis/Command/DictDel.php
new file mode 100644
index 0000000..c4e49e8
--- /dev/null
+++ b/src/Redis/Command/DictDel.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method int parseResponse(mixed $data)
+ */
+class DictDel extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dict' => new NamelessOption(null, '>=2.0.0'),
+            'terms' => [],
+        ], $rediSearchVersion);
+    }
+
+    public function addTerm(string $term): self
+    {
+        $this->options['terms'][] = new NamelessOption($term);
+
+        return $this;
+    }
+
+    public function addTerms(string ...$term): self
+    {
+        array_walk($term, function (string $term): void {
+            $this->addTerm($term);
+        });
+
+        return $this;
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dict']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.DICTDEL';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dict', 'terms'];
+    }
+}
diff --git a/src/Redis/Command/DictDump.php b/src/Redis/Command/DictDump.php
new file mode 100644
index 0000000..f8a9750
--- /dev/null
+++ b/src/Redis/Command/DictDump.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method array<string> parseResponse(mixed $data)
+ */
+class DictDump extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dict' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dict']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.DICTDUMP';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dict'];
+    }
+}
diff --git a/src/Redis/Command/DropIndex.php b/src/Redis/Command/DropIndex.php
new file mode 100644
index 0000000..890b1c7
--- /dev/null
+++ b/src/Redis/Command/DropIndex.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Predis\Response\Status;
+
+/**
+ * @method Status parseResponse(mixed $data)
+ */
+class DropIndex extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'dd' => new FlagOption('DD', false, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setDeleteDocument(bool $active = true): self
+    {
+        $this->options['dd']->setActive($active);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.DROPINDEX';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index'];
+    }
+}
diff --git a/src/Redis/Command/Explain.php b/src/Redis/Command/Explain.php
new file mode 100644
index 0000000..5be51eb
--- /dev/null
+++ b/src/Redis/Command/Explain.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method string parseResponse(mixed $data)
+ */
+class Explain extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'query' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setQuery(string $query): self
+    {
+        $this->options['query']->setValue($query);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.EXPLAIN';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'query'];
+    }
+}
diff --git a/src/Index/Builder/NumericField.php b/src/Redis/Command/ExplainCli.php
similarity index 85%
rename from src/Index/Builder/NumericField.php
rename to src/Redis/Command/ExplainCli.php
index 9bb9a1b..0b303b5 100644
--- a/src/Index/Builder/NumericField.php
+++ b/src/Redis/Command/ExplainCli.php
@@ -19,12 +19,15 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Builder;
+namespace MacFJA\RediSearch\Redis\Command;
 
-class NumericField extends AbstractField
+/**
+ * @method array<string> parseResponse(mixed $data)
+ */
+class ExplainCli extends Explain
 {
-    public function getType(): string
+    public function getId()
     {
-        return self::TYPE_NUMERIC;
+        return 'FT.EXPLAINCLI';
     }
 }
diff --git a/src/Redis/Command/IndexList.php b/src/Redis/Command/IndexList.php
new file mode 100644
index 0000000..d47b9aa
--- /dev/null
+++ b/src/Redis/Command/IndexList.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use Predis\Command\Command;
+
+/**
+ * @method array<string> parseResponse(mixed $data)
+ */
+class IndexList extends Command
+{
+    public function getId()
+    {
+        return 'FT._LIST';
+    }
+}
diff --git a/src/Redis/Command/Info.php b/src/Redis/Command/Info.php
new file mode 100644
index 0000000..b736bfa
--- /dev/null
+++ b/src/Redis/Command/Info.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Response\InfoResponse;
+
+/**
+ * @method InfoResponse parseResponse(mixed $data)
+ */
+class Info extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $name): self
+    {
+        $this->options['index']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.INFO';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index'];
+    }
+
+    protected function transformParsedResponse($data)
+    {
+        return new InfoResponse($data);
+    }
+}
diff --git a/src/Redis/Command/LanguageOptionTrait.php b/src/Redis/Command/LanguageOptionTrait.php
new file mode 100644
index 0000000..cdd11cd
--- /dev/null
+++ b/src/Redis/Command/LanguageOptionTrait.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+
+trait LanguageOptionTrait
+{
+    /**
+     * @return array<CV<NamedOption>>
+     */
+    protected function getLanguageOptions(): array
+    {
+        $base = ['arabic', 'danish', 'dutch', 'english', 'finnish', 'french', 'german', 'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian', 'russian', 'spanish', 'swedish', 'tamil', 'turkish', 'chinese'];
+        $v20006 = array_merge($base, ['basque', 'catalan', 'greek', 'indonesian', 'irish', 'lithuanian', 'nepali']);
+        $c200010 = array_merge($v20006, ['armenian', 'serbian', 'yiddish']);
+
+        return [
+            CV::allowedValues(new NamedOption('LANGUAGE', null, '>=2.0.0 <2.0.6'), $base),
+            CV::allowedValues(new NamedOption('LANGUAGE', null, '>=2.0.6 <2.0.10'), $v20006),
+            CV::allowedValues(new NamedOption('LANGUAGE', null, '^2.0.10'), $c200010),
+        ];
+    }
+}
diff --git a/src/Search/Highlight.php b/src/Redis/Command/Option/AbstractCommandOption.php
similarity index 51%
rename from src/Search/Highlight.php
rename to src/Redis/Command/Option/AbstractCommandOption.php
index fe499f0..30f5fc8 100644
--- a/src/Search/Highlight.php
+++ b/src/Redis/Command/Option/AbstractCommandOption.php
@@ -19,44 +19,50 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search;
+namespace MacFJA\RediSearch\Redis\Command\Option;
 
-use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
+use Composer\Semver\Semver;
+use UnexpectedValueException;
 
-class Highlight implements \MacFJA\RediSearch\PartialQuery
+abstract class AbstractCommandOption implements CommandOption
 {
-    /** @var array<string> */
-    private $fields = [];
-
     /** @var null|string */
-    private $openTag;
+    private $versionConstraint;
 
-    /** @var null|string */
-    private $closeTag;
+    public function __construct(?string $versionConstraint = null)
+    {
+        $this->versionConstraint = $versionConstraint;
+    }
 
-    /**
-     * @param array<string> $fields
-     */
-    public function __construct(array $fields = [], ?string $openTag = null, ?string $closeTag = null)
+    public function isCompatible(?string $version): bool
     {
-        DataHelper::assertArrayOf($fields, 'string');
-        $this->fields = $fields;
-        $this->openTag = $openTag;
-        $this->closeTag = $closeTag;
+        if (null === $version) {
+            return true;
+        }
+
+        try {
+            return Semver::satisfies($version, $this->versionConstraint ?? '*');
+        } catch (UnexpectedValueException $exception) {
+            return false;
+        }
+    }
+
+    public function getVersionConstraint(): string
+    {
+        return $this->versionConstraint ?? '*';
     }
 
-    public function getQueryParts(): array
+    public function render(?string $version = null): array
     {
-        $query = ['HIGHLIGHT'];
-        $query = RedisHelper::buildQueryList($query, ['FIELDS' => $this->fields]);
-        if (is_string($this->openTag) && is_string($this->closeTag)) {
-            $query[] = 'TAGS';
-            $query[] = $this->openTag;
-            $query[] = $this->closeTag;
+        if (!$this->isCompatible($version) || !$this->isValid()) {
+            return [];
         }
 
-        return $query;
+        return array_filter($this->doRender($version), static function ($item) { return is_scalar($item); });
     }
+
+    /**
+     * @return array<null|float|int|string>
+     */
+    abstract protected function doRender(?string $version): array;
 }
diff --git a/src/PartialQuery.php b/src/Redis/Command/Option/CommandOption.php
similarity index 76%
rename from src/PartialQuery.php
rename to src/Redis/Command/Option/CommandOption.php
index 9c6d1a8..943e9fe 100644
--- a/src/PartialQuery.php
+++ b/src/Redis/Command/Option/CommandOption.php
@@ -19,12 +19,23 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch;
+namespace MacFJA\RediSearch\Redis\Command\Option;
 
-interface PartialQuery
+interface CommandOption
 {
     /**
      * @return array<float|int|string>
      */
-    public function getQueryParts(): array;
+    public function render(?string $version = null): array;
+
+    public function isValid(): bool;
+
+    /**
+     * @return mixed
+     */
+    public function getOptionData();
+
+    public function isCompatible(?string $version): bool;
+
+    public function getVersionConstraint(): string;
 }
diff --git a/src/Redis/Command/Option/CustomValidatorOption.php b/src/Redis/Command/Option/CustomValidatorOption.php
new file mode 100644
index 0000000..02c8736
--- /dev/null
+++ b/src/Redis/Command/Option/CustomValidatorOption.php
@@ -0,0 +1,90 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+use Respect\Validation\Rules\In;
+use Respect\Validation\Rules\NumericVal;
+use Respect\Validation\Validatable;
+
+/**
+ * @template T of CommandOption
+ * @mixin T
+ */
+class CustomValidatorOption implements CommandOption, DecoratedCommandOption
+{
+    use DecoratedOptionTrait { render as decoratedRender; }
+
+    /** @var Validatable */
+    private $validator;
+
+    /**
+     * @phpstan-param T $decorated
+     */
+    public function __construct(CommandOption $decorated, Validatable $validator)
+    {
+        $this->validator = $validator;
+        $this->setDecoratedOption($decorated);
+    }
+
+    /**
+     * @phpstan-param T $decorated
+     * @psalm-param CommandOption $decorated
+     *
+     * @param array<float|int|string> $allowed
+     *
+     * @return CustomValidatorOption<T>
+     */
+    public static function allowedValues(CommandOption $decorated, array $allowed): self
+    {
+        return new self($decorated, new In($allowed));
+    }
+
+    /**
+     * @phpstan-param T $decorated
+     * @psalm-param CommandOption $decorated
+     *
+     * @return static<T>
+     */
+    public static function isNumeric(CommandOption $decorated): self
+    {
+        return new self($decorated, new NumericVal());
+    }
+
+    public function isValid(): bool
+    {
+        return $this->validator->validate($this->getDecoratedOption()->getOptionData());
+    }
+
+    public function getValidator(): Validatable
+    {
+        return $this->validator;
+    }
+
+    public function render(?string $version = null): array
+    {
+        if ($this->isValid()) {
+            return $this->decoratedRender($version);
+        }
+
+        return [];
+    }
+}
diff --git a/alias/Search.php b/src/Redis/Command/Option/DecoratedCommandOption.php
similarity index 87%
rename from alias/Search.php
rename to src/Redis/Command/Option/DecoratedCommandOption.php
index 8c764a3..e61863a 100644
--- a/alias/Search.php
+++ b/src/Redis/Command/Option/DecoratedCommandOption.php
@@ -19,9 +19,9 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RedisSearch;
+namespace MacFJA\RediSearch\Redis\Command\Option;
 
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Search */
-final class Search extends \MacFJA\RediSearch\Search
+interface DecoratedCommandOption extends CommandOption
 {
+    public function getDecoratedOption(): CommandOption;
 }
diff --git a/src/Redis/Command/Option/DecoratedOptionAwareTrait.php b/src/Redis/Command/Option/DecoratedOptionAwareTrait.php
new file mode 100644
index 0000000..29de241
--- /dev/null
+++ b/src/Redis/Command/Option/DecoratedOptionAwareTrait.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+trait DecoratedOptionAwareTrait
+{
+    /**
+     * @param object|string $object
+     */
+    protected function methodExists($object, string $method): bool
+    {
+        if ($object instanceof DecoratedCommandOption) {
+            return $this->methodExists($object->getDecoratedOption(), $method);
+        }
+
+        return method_exists($object, $method);
+    }
+}
diff --git a/src/Redis/Command/Option/DecoratedOptionTrait.php b/src/Redis/Command/Option/DecoratedOptionTrait.php
new file mode 100644
index 0000000..4640833
--- /dev/null
+++ b/src/Redis/Command/Option/DecoratedOptionTrait.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+use BadMethodCallException;
+
+/**
+ * @template T of CommandOption
+ * @mixin T
+ */
+trait DecoratedOptionTrait
+{
+    /**
+     * @var CommandOption
+     * @phpstan-var T
+     */
+    private $decorated;
+
+    /**
+     * @param array<mixed> $arguments
+     *
+     * @return mixed
+     */
+    public function __call(string $name, array $arguments)
+    {
+        if (method_exists($this->decorated, $name)) {
+            return $this->decorated->{$name}(...$arguments);
+        }
+
+        throw new BadMethodCallException('Call undefined method '.$name);
+    }
+
+    public function isCompatible(?string $version): bool
+    {
+        return $this->decorated->isCompatible($version);
+    }
+
+    public function getVersionConstraint(): string
+    {
+        return $this->decorated->getVersionConstraint();
+    }
+
+    public function render(?string $version = null): array
+    {
+        return $this->decorated->render($version);
+    }
+
+    public function isValid(): bool
+    {
+        return $this->decorated->isValid();
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getOptionData()
+    {
+        return $this->decorated->getOptionData();
+    }
+
+    /**
+     * @phpstan-return T
+     */
+    public function getDecoratedOption(): CommandOption
+    {
+        return $this->decorated;
+    }
+
+    /**
+     * @phpstan-param T $decorated
+     */
+    private function setDecoratedOption(CommandOption $decorated): void
+    {
+        $this->decorated = $decorated;
+    }
+}
diff --git a/src/Redis/Command/Option/FlagOption.php b/src/Redis/Command/Option/FlagOption.php
new file mode 100644
index 0000000..c315582
--- /dev/null
+++ b/src/Redis/Command/Option/FlagOption.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+class FlagOption extends AbstractCommandOption
+{
+    /** @var string */
+    private $name;
+    /** @var bool */
+    private $active;
+
+    public function __construct(string $name, bool $active = false, ?string $versionConstraint = null)
+    {
+        $this->active = $active;
+        $this->name = $name;
+
+        parent::__construct($versionConstraint);
+    }
+
+    public function setActive(bool $active): void
+    {
+        $this->active = $active;
+    }
+
+    public function isValid(): bool
+    {
+        return true;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function isActive(): bool
+    {
+        return $this->active;
+    }
+
+    /**
+     * @return bool
+     * @SuppressWarnings(PHPMD.BooleanGetMethodName)
+     */
+    public function getOptionData()
+    {
+        return $this->isActive();
+    }
+
+    protected function doRender(?string $version): array
+    {
+        if ($this->active) {
+            return [$this->name];
+        }
+
+        return [];
+    }
+}
diff --git a/src/Redis/Command/Option/GroupedOption.php b/src/Redis/Command/Option/GroupedOption.php
new file mode 100644
index 0000000..49503bf
--- /dev/null
+++ b/src/Redis/Command/Option/GroupedOption.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+use function array_key_exists;
+use BadMethodCallException;
+use function in_array;
+use function is_bool;
+use OutOfRangeException;
+
+class GroupedOption extends AbstractCommandOption
+{
+    use DecoratedOptionAwareTrait;
+
+    /** @var array<string,CommandOption> */
+    private $options;
+    /** @var array<string> */
+    private $required;
+    /** @var array<string> */
+    private $locked;
+
+    /**
+     * @param array<string,CommandOption> $options
+     * @param array<string>               $required
+     * @param array<string>               $locked
+     */
+    public function __construct(array $options, array $required, array $locked = [], ?string $versionConstraint = null)
+    {
+        $this->required = $required;
+        $this->options = $options;
+        $this->locked = $locked;
+        parent::__construct($versionConstraint);
+    }
+
+    /**
+     * @param null|array<mixed>|mixed $value
+     *
+     * @return $this
+     */
+    public function setDataOfOption(string $name, $value): self
+    {
+        if (!array_key_exists($name, $this->options)) {
+            throw new OutOfRangeException('There is no option for '.$name);
+        }
+        if (in_array($name, $this->locked, true)) {
+            throw new BadMethodCallException('The option '.$name.' can\'t be modified');
+        }
+
+        if ($this->methodExists($this->options[$name], 'setValue')) {
+            $this->options[$name]->setValue($value);
+        }
+        if ($this->methodExists($this->options[$name], 'setActive')) {
+            $this->options[$name]->setActive($value);
+        }
+        if ($this->methodExists($this->options[$name], 'setArguments')) {
+            $this->options[$name]->setArguments($value);
+        }
+
+        return $this;
+    }
+
+    public function getOption(string $name): ?CommandOption
+    {
+        return $this->options[$name] ?? null;
+    }
+
+    /**
+     * @return null|array<mixed>|mixed
+     */
+    public function getDataOfOption(string $name)
+    {
+        $option = $this->getOption($name);
+
+        return null === $option ? null : $option->getOptionData();
+    }
+
+    /**
+     * @return array<string>
+     */
+    public function getOptionsName(): array
+    {
+        return array_keys($this->options);
+    }
+
+    public function isValid(): bool
+    {
+        foreach ($this->required as $requiredOption) {
+            if (!array_key_exists($requiredOption, $this->options) || !$this->options[$requiredOption]->isValid()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @return array<CommandOption>
+     */
+    public function getOptionData()
+    {
+        return $this->options;
+    }
+
+    protected function doRender(?string $version): array
+    {
+        $arguments = array_filter($this->options, static function (CommandOption $option) use ($version) {
+            return $option->isCompatible($version) && $option->isValid();
+        });
+        $arguments = array_reduce($arguments, static function ($carry, CommandOption $option) use ($version) {
+            return array_merge($carry, $option->render($version));
+        }, []);
+
+        return array_filter($arguments, static function ($item) { return !is_bool($item) && is_scalar($item); });
+    }
+}
diff --git a/src/Index/Exception/IndexNotFoundException.php b/src/Redis/Command/Option/NamedOption.php
similarity index 69%
rename from src/Index/Exception/IndexNotFoundException.php
rename to src/Redis/Command/Option/NamedOption.php
index a8b4b4c..fffe5da 100644
--- a/src/Index/Exception/IndexNotFoundException.php
+++ b/src/Redis/Command/Option/NamedOption.php
@@ -19,21 +19,26 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Exception;
+namespace MacFJA\RediSearch\Redis\Command\Option;
 
-use RuntimeException;
-use function sprintf;
-use Throwable;
-
-class IndexNotFoundException extends RuntimeException
+class NamedOption extends NamelessOption
 {
-    public function __construct(?string $index = null, int $code = 0, ?Throwable $previous = null)
+    /** @var string */
+    private $name;
+
+    public function __construct(string $name, $value = null, ?string $versionConstraint = null)
     {
-        if (null === $index) {
-            parent::__construct('The requested index does not exist', $code, $previous);
+        $this->name = $name;
+        parent::__construct($value, $versionConstraint);
+    }
 
-            return;
-        }
-        parent::__construct(sprintf('The index "%s" does not exist', $index), $code, $previous);
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    protected function doRender(?string $version): array
+    {
+        return [$this->name, $this->getValue()];
     }
 }
diff --git a/src/Redis/Command/Option/NamelessOption.php b/src/Redis/Command/Option/NamelessOption.php
new file mode 100644
index 0000000..5970213
--- /dev/null
+++ b/src/Redis/Command/Option/NamelessOption.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+class NamelessOption extends AbstractCommandOption
+{
+    /** @var null|mixed */
+    private $value;
+
+    /**
+     * @param null|mixed $value
+     */
+    public function __construct($value = null, ?string $versionConstraint = null)
+    {
+        $this->value = $value;
+        parent::__construct($versionConstraint);
+    }
+
+    /**
+     * @param null|mixed $value
+     */
+    public function setValue($value): void
+    {
+        $this->value = $value;
+    }
+
+    public function isValid(): bool
+    {
+        return !(null === $this->value);
+    }
+
+    /**
+     * @return null|mixed
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * @return null|mixed
+     */
+    public function getOptionData()
+    {
+        return $this->getValue();
+    }
+
+    protected function doRender(?string $version): array
+    {
+        return [$this->value];
+    }
+}
diff --git a/src/Search/QueryBuilder/AndGroup.php b/src/Redis/Command/Option/NotEmptyOption.php
similarity index 51%
rename from src/Search/QueryBuilder/AndGroup.php
rename to src/Redis/Command/Option/NotEmptyOption.php
index 4727685..c7969e9 100644
--- a/src/Search/QueryBuilder/AndGroup.php
+++ b/src/Redis/Command/Option/NotEmptyOption.php
@@ -19,48 +19,55 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Redis\Command\Option;
 
-use function array_map;
 use function count;
-use function implode;
-use function sprintf;
-use function usort;
 
-class AndGroup implements GroupPartialQuery
+/**
+ * @template T of CommandOption
+ * @mixin T
+ */
+class NotEmptyOption implements CommandOption, DecoratedCommandOption
 {
-    /** @var array<PartialQuery> */
-    private $expressions = [];
+    use DecoratedOptionTrait { isValid as decoratedIsValid; render as decoratedRender; }
 
-    public function render(): string
+    /**
+     * @phpstan-param T $decorated
+     */
+    public function __construct(CommandOption $decorated)
     {
-        usort($this->expressions, function (PartialQuery $queryA, PartialQuery $queryB) {
-            return $queryA->priority() <=> $queryB->priority();
-        });
-
-        $terms = array_map(function (PartialQuery $query): string {
-            if ($query->includeSpace()) {
-                return sprintf('(%s)', $query->render());
-            }
-
-            return $query->render();
-        }, $this->expressions);
-
-        return sprintf('%s', implode(' ', $terms));
+        $this->setDecoratedOption($decorated);
     }
 
-    public function includeSpace(): bool
+    public function render(?string $version = null): array
     {
-        return count($this->expressions) > 1;
-    }
+        if (!$this->isValid()) {
+            return [];
+        }
 
-    public function addExpression(PartialQuery $expression): void
-    {
-        $this->expressions[] = $expression;
+        return $this->decoratedRender($version);
     }
 
-    public function priority(): int
+    public function isValid(): bool
     {
-        return self::PRIORITY_NORMAL;
+        if (!$this->decoratedIsValid()) {
+            return false;
+        }
+
+        $option = $this->getDecoratedOption();
+
+        if ($option instanceof NamelessOption && empty($option->getValue())) {
+            return false;
+        }
+
+        if ($option instanceof NumberedOption && 0 === count($option->getArguments() ?? [])) {
+            return false;
+        }
+
+        if ($option instanceof FlagOption && !$option->isActive()) {
+            return false;
+        }
+
+        return true;
     }
 }
diff --git a/src/Helper/PipelineItem.php b/src/Redis/Command/Option/NumberedOption.php
similarity index 50%
rename from src/Helper/PipelineItem.php
rename to src/Redis/Command/Option/NumberedOption.php
index ba102ec..9bd725a 100644
--- a/src/Helper/PipelineItem.php
+++ b/src/Redis/Command/Option/NumberedOption.php
@@ -19,67 +19,67 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Helper;
+namespace MacFJA\RediSearch\Redis\Command\Option;
 
-use function call_user_func;
-use Predis\Command\CommandInterface;
-use Predis\Command\RawCommand;
+use function assert;
+use function count;
+use function is_array;
 
-class PipelineItem
+class NumberedOption extends AbstractCommandOption
 {
-    /** @var CommandInterface */
-    private $command;
-
-    /** @var array<mixed> */
-    private $context;
-
-    /** @var callable */
-    private $transformer;
+    /** @var string */
+    private $name;
+    /** @var null|array<float|int|string> */
+    private $arguments;
 
     /**
-     * @param array<mixed> $context
+     * @param null|array<float|int|string> $arguments
      */
-    public function __construct(CommandInterface $command, callable $transformer, array $context = [])
+    public function __construct(string $name, ?array $arguments = null, ?string $versionConstraint = null)
     {
-        $this->command = $command;
-        $this->context = $context;
-        $this->transformer = $transformer;
+        $this->arguments = $arguments;
+        $this->name = $name;
+        parent::__construct($versionConstraint);
     }
 
     /**
-     * @param array<mixed> $params
-     * @param array<mixed> $context
+     * @param array<float|int|string> $arguments
      */
-    public static function createFromRaw(array $params, callable $transformer, array $context = []): PipelineItem
+    public function setArguments(array $arguments): void
     {
-        return new self(new RawCommand($params), $transformer, $context);
+        $this->arguments = $arguments;
     }
 
-    public function getCommand(): CommandInterface
+    public function isValid(): bool
     {
-        return $this->command;
+        return is_array($this->arguments);
     }
 
-    /**
-     * @return array<mixed>
-     */
-    public function getContext(): array
+    public function getName(): string
     {
-        return $this->context;
+        return $this->name;
     }
 
-    public function getTransformer(): callable
+    /**
+     * @return null|array<float|int|string>
+     */
+    public function getArguments(): ?array
     {
-        return $this->transformer;
+        return $this->arguments;
     }
 
     /**
-     * @param mixed $rawResult
-     *
-     * @return mixed
+     * @return null|array<mixed>
      */
-    public function transform($rawResult)
+    public function getOptionData()
     {
-        return call_user_func($this->transformer, $rawResult, $this->context);
+        return $this->getArguments();
+    }
+
+    protected function doRender(?string $version): array
+    {
+        assert(is_array($this->arguments));
+
+        return array_merge([$this->name, count($this->arguments)], $this->arguments);
     }
 }
diff --git a/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php b/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
new file mode 100644
index 0000000..32f008a
--- /dev/null
+++ b/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+use BadMethodCallException;
+use function count;
+use function get_class;
+use function in_array;
+use InvalidArgumentException;
+
+trait WithPublicGroupedSetterTrait
+{
+    /**
+     * @param array<mixed> $arguments
+     *
+     * @return $this
+     */
+    public function __call(string $name, array $arguments): self
+    {
+        if (!$this instanceof GroupedOption) {
+            throw new BadMethodCallException('This method is not callable in '.get_class($this));
+        }
+        if (0 === strpos($name, 'set') && $name[3] === strtoupper($name[3])) {
+            $pascalCase = substr($name, 3);
+            $snakeCase = strtolower(
+                trim(
+                    preg_replace('/([A-Z])/', '_$1', $pascalCase) ?? $pascalCase,
+                    '_'
+                )
+            );
+
+            if (!in_array($snakeCase, $this->publicSetter(), true)) {
+                throw new BadMethodCallException(sprintf('Call undefined method %s in %s', $name, get_class($this)));
+            }
+            if (!(1 === count($arguments))) {
+                throw new InvalidArgumentException(sprintf('The method %s::%s need exactly one argument', get_class($this), $name));
+            }
+
+            return $this->setDataOfOption($snakeCase, $arguments[0]);
+        }
+
+        throw new BadMethodCallException(sprintf('Call undefined method %s is not callable in %s', $name, get_class($this)));
+    }
+
+    /**
+     * @return array<string>
+     */
+    abstract protected function publicSetter(): array;
+}
diff --git a/src/Index/Builder/Exception/MustBeSingleCharException.php b/src/Redis/Command/PaginatedCommand.php
similarity index 80%
rename from src/Index/Builder/Exception/MustBeSingleCharException.php
rename to src/Redis/Command/PaginatedCommand.php
index 12ada47..e52746f 100644
--- a/src/Index/Builder/Exception/MustBeSingleCharException.php
+++ b/src/Redis/Command/PaginatedCommand.php
@@ -19,10 +19,15 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Builder\Exception;
+namespace MacFJA\RediSearch\Redis\Command;
 
-use UnexpectedValueException;
+use Predis\Command\CommandInterface;
 
-class MustBeSingleCharException extends UnexpectedValueException
+interface PaginatedCommand extends CommandInterface
 {
+    public function getOffset(): ?int;
+
+    public function getSize(): ?int;
+
+    public function setLimit(int $offset, int $size): PaginatedCommand;
 }
diff --git a/src/Aggregate/Exception/UnknownSortDirectionException.php b/src/Redis/Command/PrefixFieldName.php
similarity index 68%
rename from src/Aggregate/Exception/UnknownSortDirectionException.php
rename to src/Redis/Command/PrefixFieldName.php
index 052f55b..e4ffb94 100644
--- a/src/Aggregate/Exception/UnknownSortDirectionException.php
+++ b/src/Redis/Command/PrefixFieldName.php
@@ -19,21 +19,28 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Aggregate\Exception;
+namespace MacFJA\RediSearch\Redis\Command;
 
-use function implode;
-use function sprintf;
-use Throwable;
-use UnexpectedValueException;
-
-class UnknownSortDirectionException extends UnexpectedValueException
+trait PrefixFieldName
 {
     /**
-     * @param array<string> $directions
+     * @param array<string> $fields
+     *
+     * @return array<string>
      */
-    public function __construct(array $directions, int $code = 0, ?Throwable $previous = null)
+    protected static function prefixFields(array $fields): array
     {
-        $message = sprintf('Sort By direction can only be "ASC" or "DESC". Provided: %s.', implode(', ', $directions));
-        parent::__construct($message, $code, $previous);
+        return array_map(static function (string $field): string {
+            return self::prefixField($field);
+        }, $fields);
+    }
+
+    protected static function prefixField(string $field): string
+    {
+        if (0 === strpos($field, '@')) {
+            return $field;
+        }
+
+        return '@'.$field;
     }
 }
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
new file mode 100644
index 0000000..44da21b
--- /dev/null
+++ b/src/Redis/Command/Search.php
@@ -0,0 +1,358 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use function assert;
+use function count;
+use InvalidArgumentException;
+use function is_array;
+use function is_int;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption;
+use MacFJA\RediSearch\Redis\Response\ArrayResponseTrait;
+use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
+use MacFJA\RediSearch\Redis\Response\SearchResponseItem;
+
+/**
+ * @method PaginatedResponse<SearchResponseItem> parseResponse(mixed $data)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
+class Search extends AbstractCommand implements PaginatedCommand
+{
+    use LanguageOptionTrait;
+    use ArrayResponseTrait;
+
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct(
+            [
+                'index' => new NamelessOption(null, '>=2.0.0'),
+                'query' => new NamelessOption(null, '>=2.0.0'),
+                'nocontent' => new FlagOption('NOCONTENT', false, '>= 2.0.0'),
+                'verbatim' => new FlagOption('VERBATIM', false, '>= 2.0.0'),
+                'nostopwords' => new FlagOption('NOSTOPWORDS', false, '>= 2.0.0'),
+                'withscores' => new FlagOption('WITHSCORES', false, '>= 2.0.0'),
+                'withpayloads' => new FlagOption('WITHPAYLOADS', false, '>= 2.0.0'),
+                'withsortkeys' => new FlagOption('WITHSORTKEYS', false, '>= 2.0.0'),
+                'filter' => [],
+                'geofilter' => new GeoFilterOption(),
+                'inkeys' => new NumberedOption('INKEYS', null, '>=2.0.0'),
+                'infields' => new NumberedOption('INFIELDS', null, '>=2.0.0'),
+                'return' => new NumberedOption('RETURN', null, '>=2.0.0'),
+                'summarize' => new SummarizeOption(),
+                'highlight' => new HighlightOption(),
+                'slop' => CV::isNumeric(new NamedOption('SLOP', null, '>=2.0.0')),
+                'inorder' => new FlagOption('INORDER', false, '>=2.0.0'),
+                'language' => $this->getLanguageOptions(),
+                'expander' => new NamedOption('EXPANDER', null, '>=2.0.0'),
+                'scorer' => new NamedOption('SCORER', null, '>=2.0.0'),
+                'explainscore' => new FlagOption('EXPLAINSCORE', false, '>=2.0.0'),
+                'payload' => new NamedOption('PAYLOAD', null, '>=2.0.0'),
+                'sortby' => new SortByOption(),
+                'limit' => new LimitOption(),
+            ],
+            $rediSearchVersion
+        );
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setQuery(string $query): self
+    {
+        $this->options['query']->setValue($query);
+
+        return $this;
+    }
+
+    public function setNoContent(bool $active = true): self
+    {
+        $this->options['nocontent']->setActive($active);
+
+        return $this;
+    }
+
+    public function setVerbatim(bool $active = true): self
+    {
+        $this->options['verbatim']->setActive($active);
+
+        return $this;
+    }
+
+    public function setNoStopWords(bool $active = true): self
+    {
+        $this->options['nostopwords']->setActive($active);
+
+        return $this;
+    }
+
+    public function setWithScores(bool $active = true): self
+    {
+        $this->options['withscores']->setActive($active);
+
+        return $this;
+    }
+
+    public function setWithPayloads(bool $active = true): self
+    {
+        $this->options['withpayloads']->setActive($active);
+
+        return $this;
+    }
+
+    public function setWithSortKeys(bool $active = true): self
+    {
+        $this->options['withsortkeys']->setActive($active);
+
+        return $this;
+    }
+
+    /**
+     * @param null|array<string> $fields
+     *
+     * @return $this
+     */
+    public function setSummarize(?array $fields = null, ?int $fragmentCount = null, ?int $fragmentSize = null, ?string $separator = null): self
+    {
+        $this->options['summarize']
+            ->setDataOfOption('fields', $fields)
+            ->setDataOfOption('frags', $fragmentCount)
+            ->setDataOfOption('len', $fragmentSize)
+            ->setDataOfOption('separator', $separator)
+        ;
+
+        return $this;
+    }
+
+    /**
+     * @param null|array<string> $fields
+     *
+     * @return $this
+     */
+    public function setHighlight(?array $fields, ?string $openTag, ?string $closeTag): self
+    {
+        $this->options['highlight']
+            ->setDataOfOption('fields', $fields)
+            ->setTags($openTag, $closeTag)
+        ;
+
+        return $this;
+    }
+
+    public function setSlop(int $slop): self
+    {
+        $this->options['slop']->setValue($slop);
+
+        return $this;
+    }
+
+    public function setInOrder(bool $active = true): self
+    {
+        $this->options['inorder']->setActive($active);
+
+        return $this;
+    }
+
+    public function setLanguage(string $language): self
+    {
+        array_walk($this->options['language'], static function (CV $option) use ($language): void {
+            $option->setValue($language);
+        });
+
+        return $this;
+    }
+
+    public function setExpander(string $function): self
+    {
+        $this->options['expander']->setValue($function);
+
+        return $this;
+    }
+
+    public function setScorer(string $function): self
+    {
+        $this->options['scorer']->setValue($function);
+
+        return $this;
+    }
+
+    public function setExplainScore(bool $active = true): self
+    {
+        $this->options['explainscore']->setActive($active);
+
+        return $this;
+    }
+
+    public function setPayload(string $payload): self
+    {
+        $this->options['payload']->setValue($payload);
+
+        return $this;
+    }
+
+    public function setSortBy(string $field, ?string $direction = null): self
+    {
+        $this->options['sortby']->setField($field)->setDirection($direction);
+
+        return $this;
+    }
+
+    /**
+     * @return $this
+     */
+    public function setLimit(int $offset, int $size): PaginatedCommand
+    {
+        $this->options['limit']->setOffset($offset)->setSize($size);
+
+        return $this;
+    }
+
+    public function addFilter(FilterOption $filterOption): self
+    {
+        $this->options['filter'][] = $filterOption;
+
+        return $this;
+    }
+
+    public function setGeoFilter(string $fieldName, float $longitude, float $latitude, float $radius, string $unit): self
+    {
+        $this->options['geofilter']->setDataOfOption('field', $fieldName)
+            ->setDataOfOption('lon', $longitude)
+            ->setDataOfOption('lat', $latitude)
+            ->setDataOfOption('radius', $radius)
+            ->setDataOfOption('unit', $unit)
+        ;
+
+        return $this;
+    }
+
+    public function setInKeys(string ...$key): self
+    {
+        $this->options['inkeys']->setArguments($key);
+
+        return $this;
+    }
+
+    public function setInFields(string ...$field): self
+    {
+        $this->options['infields']->setArguments($field);
+
+        return $this;
+    }
+
+    public function setReturn(string ...$field): self
+    {
+        $this->options['return']->setArguments($field);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SEARCH';
+    }
+
+    public function getOffset(): ?int
+    {
+        /** @var LimitOption $limit */
+        $limit = $this->options['limit'];
+
+        return $limit->getOffset();
+    }
+
+    public function getSize(): ?int
+    {
+        /** @var LimitOption $limit */
+        $limit = $this->options['limit'];
+
+        return $limit->getSize();
+    }
+
+    /**
+     * @param mixed $data
+     *
+     * @return mixed|PaginatedResponse
+     *
+     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+     * @SuppressWarnings(PHPMD.NPathComplexity)
+     */
+    public function transformParsedResponse($data)
+    {
+        if (!is_array($data)) {
+            return $data;
+        }
+
+        $totalCount = array_shift($data);
+        assert(is_int($totalCount));
+
+        $useScores = $this->options['withscores']->isActive();
+        $usePayloads = $this->options['withpayloads']->isActive();
+        $useSortKeys = $this->options['withsortkeys']->isActive();
+        $noContent = $this->options['nocontent']->isActive();
+
+        $chunkSize = 2 // Hash + fields
+            + (true === $useScores ? 1 : 0)
+            + (true === $usePayloads ? 1 : 0)
+            + (true === $useSortKeys ? 1 : 0);
+
+        $documents = array_chunk($data, $chunkSize);
+
+        $items = array_map(static function ($document) use ($useSortKeys, $usePayloads, $useScores, $noContent) {
+            $hash = array_shift($document) ?? '';
+            $score = true === $useScores ? (float) array_shift($document) : null;
+            $payload = true === $usePayloads ? array_shift($document) : null;
+            $sortKey = true === $useSortKeys ? array_shift($document) : null;
+
+            $fields = [];
+            if (false === $noContent) {
+                if (!(1 === count($document))) {
+                    throw new InvalidArgumentException();
+                }
+                $rawData = reset($document);
+                assert(is_array($rawData));
+                $fields = self::getPairs($rawData);
+            }
+
+            return new SearchResponseItem($hash, $fields, $score, $payload, $sortKey);
+        }, $documents);
+
+        return new PaginatedResponse($this, $totalCount, $items);
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'query'];
+    }
+}
diff --git a/src/Redis/Command/SearchCommand/FilterOption.php b/src/Redis/Command/SearchCommand/FilterOption.php
new file mode 100644
index 0000000..618bb2e
--- /dev/null
+++ b/src/Redis/Command/SearchCommand/FilterOption.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+
+class FilterOption extends AbstractCommandOption
+{
+    use NumericRangeTrait;
+
+    /** @var string */
+    private $fieldName;
+    /** @var null|float */
+    private $min;
+    /** @var null|float */
+    private $max;
+    /** @var bool */
+    private $isMaxInclusive;
+    /** @var bool */
+    private $isMinInclusive;
+
+    public function __construct(string $fieldName, ?float $min, ?float $max, bool $isMinInclusive = true, bool $isMaxInclusive = true)
+    {
+        $this->fieldName = $fieldName;
+        $this->min = $min;
+        $this->max = $max;
+        $this->isMaxInclusive = $isMaxInclusive;
+        $this->isMinInclusive = $isMinInclusive;
+        parent::__construct('>=2.0.0');
+    }
+
+    public function isValid(): bool
+    {
+        return true;
+    }
+
+    /**
+     * @return array<string,null|bool|float>
+     */
+    public function getOptionData()
+    {
+        return [
+            'min' => $this->min,
+            'max' => $this->max,
+            'isMinInclusive' => $this->isMinInclusive,
+            'isMaxInclusive' => $this->isMaxInclusive,
+        ];
+    }
+
+    protected function doRender(?string $version): array
+    {
+        return array_merge(
+            ['FILTER', $this->fieldName],
+            self::renderBound($this->min, $this->max, $this->isMinInclusive, $this->isMaxInclusive)
+        );
+    }
+}
diff --git a/src/Search/GeoFilter.php b/src/Redis/Command/SearchCommand/GeoFilterOption.php
similarity index 54%
rename from src/Search/GeoFilter.php
rename to src/Redis/Command/SearchCommand/GeoFilterOption.php
index b55e76e..a9ee284 100644
--- a/src/Search/GeoFilter.php
+++ b/src/Redis/Command/SearchCommand/GeoFilterOption.php
@@ -19,53 +19,30 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search;
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
 
-use function in_array;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\PartialQuery;
-use MacFJA\RediSearch\Search\Exception\UnknownUnitException;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 
-class GeoFilter implements PartialQuery
+class GeoFilterOption extends GroupedOption
 {
+    public const UNITS = [self::UNIT_METERS, self::UNIT_KILOMETERS, self::UNIT_FEET, self::UNIT_MILES];
     public const UNIT_METERS = 'm';
-
     public const UNIT_KILOMETERS = 'km';
-
     public const UNIT_MILES = 'mi';
-
     public const UNIT_FEET = 'ft';
 
-    /** @var string */
-    private $fieldName;
-
-    /** @var float */
-    private $lon;
-
-    /** @var float */
-    private $lat;
-
-    /** @var int */
-    private $radius;
-
-    /** @var string */
-    private $unit;
-
-    public function __construct(string $fieldName, float $lon, float $lat, int $radius, string $unit)
-    {
-        DataHelper::assert(
-            in_array($unit, [self::UNIT_FEET, self::UNIT_KILOMETERS, self::UNIT_METERS, self::UNIT_MILES], true),
-            new UnknownUnitException($unit)
-        );
-        $this->fieldName = $fieldName;
-        $this->lon = $lon;
-        $this->lat = $lat;
-        $this->radius = $radius;
-        $this->unit = $unit;
-    }
-
-    public function getQueryParts(): array
+    public function __construct()
     {
-        return ['GEOFILTER', $this->fieldName, $this->lon, $this->lat, $this->radius, $this->unit];
+        parent::__construct([
+            'type' => new FlagOption('GEOFILTER', true, '>=2.0.0'),
+            'field' => new NamelessOption(null, '>=2.0.0'),
+            'lon' => CV::isNumeric(new NamelessOption(null, '>=2.0.0')),
+            'lat' => CV::isNumeric(new NamelessOption(null, '>=2.0.0')),
+            'radius' => CV::isNumeric(new NamelessOption(null, '>=2.0.0')),
+            'unit' => CV::allowedValues(new NamelessOption(null, '>=2.0.0'), self::UNITS),
+        ], ['type', 'field', 'lon', 'lat', 'radius', 'unit'], ['type'], '>=2.0.0');
     }
 }
diff --git a/src/Redis/Command/SearchCommand/HighlightOption.php b/src/Redis/Command/SearchCommand/HighlightOption.php
new file mode 100644
index 0000000..b81da3f
--- /dev/null
+++ b/src/Redis/Command/SearchCommand/HighlightOption.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+
+class HighlightOption extends GroupedOption
+{
+    public function __construct()
+    {
+        parent::__construct([
+            'type' => new FlagOption('HIGHLIGHT', true, '>=2.0.0'),
+            'fields' => new NotEmptyOption(new NumberedOption('FIELDS', null, '>=2.0.0')),
+            'tag' => new GroupedOption([
+                'type' => new FlagOption('TAGS', true, '>=2.0.0'),
+                'open' => new NamelessOption(null, '>=2.0.0'),
+                'close' => new NamelessOption(null, '>=2.0.0'),
+            ], ['type', 'open', 'close'], ['type'], '>=2.0.0'),
+        ], ['type'], ['type'], '>=2.0.0');
+    }
+
+    public function setTags(?string $open, ?string $close): self
+    {
+        /** @var GroupedOption $tag */
+        $tag = $this->getOption('tag');
+        $tag->setDataOfOption('open', $open)
+            ->setDataOfOption('close', $close)
+        ;
+
+        return $this;
+    }
+}
diff --git a/src/Pipeline.php b/src/Redis/Command/SearchCommand/LimitOption.php
similarity index 50%
rename from src/Pipeline.php
rename to src/Redis/Command/SearchCommand/LimitOption.php
index 70453ac..e9e585f 100644
--- a/src/Pipeline.php
+++ b/src/Redis/Command/SearchCommand/LimitOption.php
@@ -19,61 +19,65 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch;
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
 
-use function array_keys;
-use function array_map;
-use function assert;
-use function is_array;
-use MacFJA\RediSearch\Helper\PipelineItem;
-use Predis\Client;
+use function is_int;
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 
-class Pipeline
+class LimitOption extends AbstractCommandOption
 {
-    /** @var array<PipelineItem> */
-    private $pipeline;
+    /** @var null|int */
+    private $offset;
+    /** @var null|int */
+    private $size;
 
-    /** @var Client */
-    private $redis;
-
-    public function __construct(Client $redis)
+    public function __construct()
     {
-        $this->redis = $redis;
-        $this->pipeline = [];
+        parent::__construct('>=2.0.0');
     }
 
-    public function addItem(PipelineItem $pipelineItem): self
+    public function setOffset(?int $offset): LimitOption
     {
-        $this->pipeline[] = $pipelineItem;
+        $this->offset = $offset;
 
         return $this;
     }
 
-    public function addPipeable(Pipeable $pipeable): self
+    public function getOffset(): ?int
+    {
+        return $this->offset;
+    }
+
+    public function getSize(): ?int
+    {
+        return $this->size;
+    }
+
+    public function setSize(?int $size): LimitOption
     {
-        $this->pipeline[] = $pipeable->asPipelineItem();
+        $this->size = $size;
 
         return $this;
     }
 
+    public function isValid(): bool
+    {
+        return is_int($this->offset) && is_int($this->size) && $this->offset >= 0 && $this->size >= 0;
+    }
+
     /**
-     * @return array<mixed>
+     * @return array<string,null|int>
      */
-    public function executePipeline(): array
+    public function getOptionData()
     {
-        $rawReplies = $this->redis->pipeline(function (\Predis\Pipeline\Pipeline $pipe) {
-            foreach ($this->pipeline as $item) {
-                $pipe->executeCommand($item->getCommand());
-            }
-        });
-        assert(is_array($rawReplies));
+        return [
+            'offset' => $this->offset,
+            'size' => $this->size,
+        ];
+    }
 
-        try {
-            return array_map(function ($reply, $index) {
-                return $this->pipeline[$index]->transform($reply);
-            }, $rawReplies, array_keys($rawReplies));
-        } finally {
-            $this->pipeline = [];
-        }
+    protected function doRender(?string $version): array
+    {
+        return ['LIMIT', $this->offset, $this->size];
     }
 }
diff --git a/src/Search/QueryBuilder/NumericFacet.php b/src/Redis/Command/SearchCommand/NumericRangeTrait.php
similarity index 59%
rename from src/Search/QueryBuilder/NumericFacet.php
rename to src/Redis/Command/SearchCommand/NumericRangeTrait.php
index 4927a2b..d315f22 100644
--- a/src/Search/QueryBuilder/NumericFacet.php
+++ b/src/Redis/Command/SearchCommand/NumericRangeTrait.php
@@ -19,38 +19,10 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search\QueryBuilder;
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
 
-use function is_numeric;
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use function sprintf;
-
-class NumericFacet implements PartialQuery
+trait NumericRangeTrait
 {
-    /** @var string */
-    private $fieldName;
-
-    /** @var null|float */
-    private $min;
-
-    /** @var null|float */
-    private $max;
-
-    /** @var bool */
-    private $isMaxInclusive;
-
-    /** @var bool */
-    private $isMinInclusive;
-
-    public function __construct(string $fieldName, ?float $min, ?float $max, bool $isMinInclusive = true, bool $isMaxInclusive = true)
-    {
-        $this->fieldName = $fieldName;
-        $this->min = $min;
-        $this->max = $max;
-        $this->isMaxInclusive = $isMaxInclusive;
-        $this->isMinInclusive = $isMinInclusive;
-    }
-
     public static function greaterThan(string $fieldName, float $value): self
     {
         return new self($fieldName, $value, null, false);
@@ -76,28 +48,21 @@ public static function equalsTo(string $fieldName, float $value): self
         return new self($fieldName, $value, $value, true, true);
     }
 
-    public function render(): string
+    /**
+     * @return string[]
+     */
+    private static function renderBound(?float $min, ?float $max, bool $isMinInclusive = true, bool $isMaxInclusive = true): array
     {
-        $min = '-inf';
-        $max = '+inf';
+        $minValue = '-inf';
+        $maxValue = '+inf';
 
-        if (is_numeric($this->min)) {
-            $min = ($this->isMinInclusive ? '' : '(').$this->min;
+        if (is_numeric($min)) {
+            $minValue = (true === $isMinInclusive ? '' : '(').$min;
         }
-        if (is_numeric($this->max)) {
-            $max = ($this->isMaxInclusive ? '' : '(').$this->max;
+        if (is_numeric($max)) {
+            $maxValue = (true === $isMaxInclusive ? '' : '(').$max;
         }
 
-        return sprintf('@%s:[%s %s]', EscapeHelper::escapeFieldName($this->fieldName), $min, $max);
-    }
-
-    public function includeSpace(): bool
-    {
-        return false;
-    }
-
-    public function priority(): int
-    {
-        return self::PRIORITY_NORMAL;
+        return [$minValue, $maxValue];
     }
 }
diff --git a/src/Redis/Command/SearchCommand/SortByOption.php b/src/Redis/Command/SearchCommand/SortByOption.php
new file mode 100644
index 0000000..b565b4f
--- /dev/null
+++ b/src/Redis/Command/SearchCommand/SortByOption.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+class SortByOption extends GroupedOption
+{
+    public function __construct()
+    {
+        parent::__construct([
+            'type' => new FlagOption('SORTBY', true, '>=2.0.0'),
+            'field' => new NamelessOption(null, '>=2.0.0'),
+            'direction' => CV::allowedValues(new NamelessOption(null, '>=2.0.0'), ['DESC', 'ASC']),
+        ], ['type', 'field'], ['type'], '>=2.0.0');
+    }
+
+    public function setField(string $name): self
+    {
+        $this->setDataOfOption('field', $name);
+
+        return $this;
+    }
+
+    public function setDirection(?string $direction): self
+    {
+        $this->setDataOfOption('direction', $direction);
+
+        return $this;
+    }
+}
diff --git a/src/Redis/Command/SearchCommand/SummarizeOption.php b/src/Redis/Command/SearchCommand/SummarizeOption.php
new file mode 100644
index 0000000..b768678
--- /dev/null
+++ b/src/Redis/Command/SearchCommand/SummarizeOption.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+
+class SummarizeOption extends GroupedOption
+{
+    public function __construct()
+    {
+        parent::__construct([
+            'type' => new FlagOption('SUMMARIZE', true, '>=2.0.0'),
+            'fields' => new NotEmptyOption(new NumberedOption('FIELDS', null, '>=2.0.0')),
+            'frags' => CV::isNumeric(new NamedOption('FRAGS', null, '>=2.0.0')),
+            'len' => CV::isNumeric(new NamedOption('LEN', null, '>=2.0.0')),
+            'separator' => new NamedOption('SEPARATOR', null, '>=2.0.à'),
+        ], ['type'], ['type'], '>=2.0.0');
+    }
+}
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
new file mode 100644
index 0000000..e080586
--- /dev/null
+++ b/src/Redis/Command/SpellCheck.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use function is_array;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption;
+use MacFJA\RediSearch\Redis\Response\SpellCheckResponseItem;
+use Respect\Validation\Validator;
+
+/**
+ * @method array<SpellCheckResponseItem> parseResponse(mixed $data)
+ */
+class SpellCheck extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'query' => new NamelessOption(null, '>=2.0.0'),
+            'distance' => new CustomValidatorOption(
+                new NamedOption('DISTANCE', null, '>=2.0.0'),
+                Validator::intVal()->between(1, 4)
+            ),
+            'terms' => [],
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setQuery(string $query): self
+    {
+        $this->options['query']->setValue($query);
+
+        return $this;
+    }
+
+    public function setDistance(int $distance): self
+    {
+        $this->options['distance']->setValue($distance);
+
+        return $this;
+    }
+
+    public function addTerms(string $dictionary, bool $isExcluding = false): self
+    {
+        $terms = new TermsOption();
+        $terms->setIsExclusion($isExcluding);
+        $terms->setDictionary($dictionary);
+        $this->options['terms'][] = $terms;
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SPELLCHECK';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'query'];
+    }
+
+    /**
+     * @param mixed $data
+     *
+     * @return mixed|SpellCheckResponseItem[]
+     */
+    protected function transformParsedResponse($data)
+    {
+        if (!is_array($data)) {
+            return $data;
+        }
+
+        $items = array_map(static function (array $group) {
+            $term = array_shift($group);
+            if (!('TERM' === $term)) {
+                return null;
+            }
+            $misspelled = array_shift($group);
+            $suggestions = reset($group);
+            $suggestions = array_column($suggestions, 0, 1);
+
+            return new SpellCheckResponseItem($misspelled, $suggestions);
+        }, $data);
+
+        return array_filter($items);
+    }
+}
diff --git a/src/Redis/Command/SpellCheckCommand/TermsOption.php b/src/Redis/Command/SpellCheckCommand/TermsOption.php
new file mode 100644
index 0000000..fcdf953
--- /dev/null
+++ b/src/Redis/Command/SpellCheckCommand/TermsOption.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\SpellCheckCommand;
+
+use function is_string;
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+
+class TermsOption extends AbstractCommandOption
+{
+    /** @var bool */
+    private $isExclusion = false;
+    /** @var string */
+    private $dictionary;
+
+    public function __construct()
+    {
+        parent::__construct('>=2.0.0');
+    }
+
+    public function setIsExclusion(bool $isExclusion): TermsOption
+    {
+        $this->isExclusion = $isExclusion;
+
+        return $this;
+    }
+
+    public function setDictionary(string $dictionary): TermsOption
+    {
+        $this->dictionary = $dictionary;
+
+        return $this;
+    }
+
+    public function isValid(): bool
+    {
+        return is_string($this->dictionary);
+    }
+
+    /**
+     * @return string
+     */
+    public function getOptionData()
+    {
+        return $this->dictionary;
+    }
+
+    protected function doRender(?string $version): array
+    {
+        return ['TERMS', $this->isExclusion ? 'EXCLUDE' : 'INCLUDE', $this->dictionary];
+    }
+}
diff --git a/src/Redis/Command/SugAdd.php b/src/Redis/Command/SugAdd.php
new file mode 100644
index 0000000..3814046
--- /dev/null
+++ b/src/Redis/Command/SugAdd.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method int parseResponse(mixed $data)
+ */
+class SugAdd extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dictionary' => new NamelessOption(null, '>=2.0.0'),
+            'suggestion' => new NamelessOption(null, '>=2.0.0'),
+            'score' => CV::isNumeric(new NamelessOption(null, '>=2.0.0')),
+            'incr' => new FlagOption('INCR', false, '>=2.0.0'),
+            'payload' => new NamedOption('PAYLOAD', null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dictionary']->setValue($name);
+
+        return $this;
+    }
+
+    public function setSuggestion(string $value): self
+    {
+        $this->options['suggestion']->setValue($value);
+
+        return $this;
+    }
+
+    public function setScore(float $score): self
+    {
+        /** @var CV<NamelessOption>|NamelessOption $option */
+        $option = $this->options['score'];
+        $option->setValue($score);
+
+        return $this;
+    }
+
+    public function setIncrement(bool $active = true): self
+    {
+        $this->options['incr']->setActive($active);
+
+        return $this;
+    }
+
+    public function setPayload(string $payload): self
+    {
+        $this->options['payload']->setValue($payload);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SUGADD';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dictionary', 'suggestion', 'score'];
+    }
+}
diff --git a/src/Redis/Command/SugDel.php b/src/Redis/Command/SugDel.php
new file mode 100644
index 0000000..d57d719
--- /dev/null
+++ b/src/Redis/Command/SugDel.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method int parseResponse(mixed $data)
+ */
+class SugDel extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dictionary' => new NamelessOption(null, '>=2.0.0'),
+            'suggestion' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dictionary']->setValue($name);
+
+        return $this;
+    }
+
+    public function setSuggestion(string $value): self
+    {
+        $this->options['suggestion']->setValue($value);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SUGDEL';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dictionary', 'suggestion'];
+    }
+}
diff --git a/src/Redis/Command/SugGet.php b/src/Redis/Command/SugGet.php
new file mode 100644
index 0000000..1edbb6c
--- /dev/null
+++ b/src/Redis/Command/SugGet.php
@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use function count;
+use InvalidArgumentException;
+use function is_array;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Response\SuggestionResponseItem;
+
+/**
+ * @method array<SuggestionResponseItem> parseResponse(mixed $data)
+ */
+class SugGet extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dictionary' => new NamelessOption(null, '>=2.0.0'),
+            'prefix' => new NamelessOption(null, '>=2.0.0'),
+            'fuzzy' => new FlagOption('FUZZY', false, '>=2.0.0'),
+            'scores' => new FlagOption('WITHSCORES', false, '>=2.0.0'),
+            'payloads' => new FlagOption('WITHPAYLOADS', false, '>=2.0.0'),
+            'max' => CV::isNumeric(new NamedOption('MAX', null, '>=2.0.0')),
+        ], $rediSearchVersion);
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dictionary']->setValue($name);
+
+        return $this;
+    }
+
+    public function setPrefix(string $value): self
+    {
+        $this->options['prefix']->setValue($value);
+
+        return $this;
+    }
+
+    public function setWithScores(bool $active = true): self
+    {
+        $this->options['scores']->setActive($active);
+
+        return $this;
+    }
+
+    public function setFuzzy(bool $active = true): self
+    {
+        $this->options['fuzzy']->setActive($active);
+
+        return $this;
+    }
+
+    public function setWithPayloads(bool $active = true): self
+    {
+        $this->options['payloads']->setActive($active);
+
+        return $this;
+    }
+
+    public function setMax(int $max): self
+    {
+        /** @var CV<NamedOption> $option */
+        $option = $this->options['max'];
+
+        $option->setValue($max);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SUGGET';
+    }
+
+    /**
+     * @param mixed $data
+     *
+     * @return mixed|SuggestionResponseItem[]
+     */
+    public function transformParsedResponse($data)
+    {
+        if (!is_array($data)) {
+            return $data;
+        }
+
+        $useScores = $this->options['scores']->isActive();
+        $usePayloads = $this->options['payloads']->isActive();
+
+        $chunkSize = 1 // value
+            + (true === $useScores ? 1 : 0)
+            + (true === $usePayloads ? 1 : 0);
+
+        $documents = array_chunk($data, $chunkSize);
+
+        return array_map(static function ($document) use ($usePayloads, $useScores) {
+            $payload = true === $usePayloads ? array_pop($document) : null;
+            $score = true === $useScores ? (float) array_pop($document) : null;
+
+            if (!(1 === count($document))) {
+                throw new InvalidArgumentException();
+            }
+            $value = reset($document);
+
+            return new SuggestionResponseItem($value, $score, $payload);
+        }, $documents);
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dictionary', 'prefix'];
+    }
+}
diff --git a/src/Redis/Command/SugLen.php b/src/Redis/Command/SugLen.php
new file mode 100644
index 0000000..2d033b6
--- /dev/null
+++ b/src/Redis/Command/SugLen.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method int parseResponse(mixed $data)
+ */
+class SugLen extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'dictionary' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setDictionary(string $name): self
+    {
+        $this->options['dictionary']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SUGLEN';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['dictionary'];
+    }
+}
diff --git a/src/Redis/Command/SynDump.php b/src/Redis/Command/SynDump.php
new file mode 100644
index 0000000..aeec8cc
--- /dev/null
+++ b/src/Redis/Command/SynDump.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+class SynDump extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SYNDUMP';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index'];
+    }
+}
diff --git a/src/Redis/Command/SynUpdate.php b/src/Redis/Command/SynUpdate.php
new file mode 100644
index 0000000..e4141da
--- /dev/null
+++ b/src/Redis/Command/SynUpdate.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+class SynUpdate extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'group' => new NamelessOption(null, '>=2.0.0'),
+            'skip' => new FlagOption('SKIPINITIALSCAN', false, '>=2.0.0'),
+            'terms' => [],
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setGroupId(string $name): self
+    {
+        $this->options['group']->setValue($name);
+
+        return $this;
+    }
+
+    public function setSkipInitialScan(bool $active = true): self
+    {
+        $this->options['skip']->setActive($active);
+
+        return $this;
+    }
+
+    public function setTerms(string ...$term): self
+    {
+        $this->options['terms'] = array_map(static function (string $term) {
+            return new NamelessOption($term);
+        }, $term);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.SYNUPDATE';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'group', 'terms'];
+    }
+}
diff --git a/src/Redis/Command/TagVals.php b/src/Redis/Command/TagVals.php
new file mode 100644
index 0000000..d8f3d6c
--- /dev/null
+++ b/src/Redis/Command/TagVals.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+
+/**
+ * @method array<string> parseResponse(mixed $data)
+ */
+class TagVals extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = '2.0.0')
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.0.0'),
+            'field' => new NamelessOption(null, '>=2.0.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    public function setField(string $name): self
+    {
+        $this->options['field']->setValue($name);
+
+        return $this;
+    }
+
+    public function getId()
+    {
+        return 'FT.TAGVALS';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'field'];
+    }
+}
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
new file mode 100644
index 0000000..78e0e51
--- /dev/null
+++ b/src/Redis/Initializer.php
@@ -0,0 +1,125 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis;
+
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\AliasAdd;
+use MacFJA\RediSearch\Redis\Command\AliasDel;
+use MacFJA\RediSearch\Redis\Command\AliasUpdate;
+use MacFJA\RediSearch\Redis\Command\Alter;
+use MacFJA\RediSearch\Redis\Command\Config;
+use MacFJA\RediSearch\Redis\Command\ConfigSet;
+use MacFJA\RediSearch\Redis\Command\Create;
+use MacFJA\RediSearch\Redis\Command\CursorDel;
+use MacFJA\RediSearch\Redis\Command\CursorRead;
+use MacFJA\RediSearch\Redis\Command\DictAdd;
+use MacFJA\RediSearch\Redis\Command\DictDel;
+use MacFJA\RediSearch\Redis\Command\DictDump;
+use MacFJA\RediSearch\Redis\Command\DropIndex;
+use MacFJA\RediSearch\Redis\Command\Explain;
+use MacFJA\RediSearch\Redis\Command\ExplainCli;
+use MacFJA\RediSearch\Redis\Command\IndexList;
+use MacFJA\RediSearch\Redis\Command\Info;
+use MacFJA\RediSearch\Redis\Command\Search;
+use MacFJA\RediSearch\Redis\Command\SpellCheck;
+use MacFJA\RediSearch\Redis\Command\SugAdd;
+use MacFJA\RediSearch\Redis\Command\SugDel;
+use MacFJA\RediSearch\Redis\Command\SugGet;
+use MacFJA\RediSearch\Redis\Command\SugLen;
+use MacFJA\RediSearch\Redis\Command\SynDump;
+use MacFJA\RediSearch\Redis\Command\SynUpdate;
+use MacFJA\RediSearch\Redis\Command\TagVals;
+use Predis\ClientInterface;
+use Predis\Profile\RedisProfile;
+
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
+class Initializer
+{
+    /**
+     * @codeCoverageIgnore
+     */
+    public static function registerCommands(RedisProfile $profile): void
+    {
+        $profile->defineCommand('ftaggregate', Aggregate::class);
+        $profile->defineCommand('ftaliasadd', AliasAdd::class);
+        $profile->defineCommand('ftaliasdel', AliasDel::class);
+        $profile->defineCommand('ftaliasupdate', AliasUpdate::class);
+        $profile->defineCommand('ftalter', Alter::class);
+        $profile->defineCommand('ftconfig', Config::class);
+        $profile->defineCommand('ftconfigset', ConfigSet::class);
+        $profile->defineCommand('ftcreate', Create::class);
+        $profile->defineCommand('ftcursordel', CursorDel::class);
+        $profile->defineCommand('ftcursorread', CursorRead::class);
+        $profile->defineCommand('ftdictadd', DictAdd::class);
+        $profile->defineCommand('ftdictdel', DictDel::class);
+        $profile->defineCommand('ftdictdump', DictDump::class);
+        $profile->defineCommand('ftdropindex', DropIndex::class);
+        $profile->defineCommand('ftexplain', Explain::class);
+        $profile->defineCommand('ftexplaincli', ExplainCli::class);
+        $profile->defineCommand('ftlist', IndexList::class);
+        $profile->defineCommand('ftinfo', Info::class);
+        $profile->defineCommand('ftsearch', Search::class);
+        $profile->defineCommand('ftspellcheck', SpellCheck::class);
+        $profile->defineCommand('ftsugadd', SugAdd::class);
+        $profile->defineCommand('ftsugdel', SugDel::class);
+        $profile->defineCommand('ftsugget', SugGet::class);
+        $profile->defineCommand('ftsuglen', SugLen::class);
+        $profile->defineCommand('ftsyndump', SynDump::class);
+        $profile->defineCommand('ftsynupdate', SynUpdate::class);
+        $profile->defineCommand('fttagvals', TagVals::class);
+    }
+
+    public static function getRediSearchVersion(ClientInterface $client): ?string
+    {
+        $modules = $client->info('Modules')['Modules'] ?? [];
+
+        foreach ($modules as $module) {
+            $data = array_column(
+                array_map(
+                    static function ($line) { return explode('=', $line); },
+                    explode(',', $module)
+                ),
+                1,
+                0
+            );
+
+            if (!(($data['name'] ?? '') === 'search') || empty($data['ver'])) {
+                continue;
+            }
+
+            $major = floor($data['ver'] / 10000);
+            $minor = floor(($data['ver'] - $major * 10000) / 100);
+            $patch = $data['ver'] - $major * 10000 - $minor * 100;
+
+            return sprintf(
+                '%d.%d.%d',
+                $major,
+                $minor,
+                $patch
+            );
+        }
+
+        return null;
+    }
+}
diff --git a/src/Aggregate/Result.php b/src/Redis/Response/AggregateResponseItem.php
similarity index 93%
rename from src/Aggregate/Result.php
rename to src/Redis/Response/AggregateResponseItem.php
index f6c626f..96c9f39 100644
--- a/src/Aggregate/Result.php
+++ b/src/Redis/Response/AggregateResponseItem.php
@@ -19,19 +19,18 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Aggregate;
+namespace MacFJA\RediSearch\Redis\Response;
 
-use Throwable;
-
-class Result
+/**
+ * @codeCoverageIgnore Simple value object
+ */
+class AggregateResponseItem
 {
     /** @var array<string,null|array<mixed>|float|int|string> */
     private $fields = [];
 
     /**
      * @param array<string,null|array<mixed>|float|int|string> $fields
-     *
-     * @throws Throwable
      */
     public function __construct(array $fields)
     {
diff --git a/src/Redis/Response/ArrayResponseTrait.php b/src/Redis/Response/ArrayResponseTrait.php
new file mode 100644
index 0000000..ed6f916
--- /dev/null
+++ b/src/Redis/Response/ArrayResponseTrait.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+use function assert;
+use function count;
+use function is_array;
+
+trait ArrayResponseTrait
+{
+    /**
+     * @param array<float|int|mixed|string> $notKeyedArray
+     *
+     * @return array<string,mixed>
+     */
+    private static function getPairs(array $notKeyedArray): array
+    {
+        if (count($notKeyedArray) % 2 > 0) {
+            array_pop($notKeyedArray);
+        }
+        $entries = array_chunk($notKeyedArray, 2);
+        $keyed = array_combine(array_column($entries, 0), array_column($entries, 1));
+        assert(is_array($keyed));
+
+        return $keyed;
+    }
+
+    /**
+     * @param array<array|mixed> $notKeyedArray
+     *
+     * @return null|array<mixed>|bool|float|int|string
+     */
+    private static function getValue(array $notKeyedArray, string $key)
+    {
+        while (count($notKeyedArray) >= 2) {
+            $shifted = (string) array_shift($notKeyedArray);
+            if ($shifted === $key) {
+                return array_shift($notKeyedArray);
+            }
+            array_shift($notKeyedArray);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param array<array|mixed> $notKeyedArray
+     *
+     * @return null|bool|float|int|string
+     */
+    private static function getNextValue(array $notKeyedArray, string $key)
+    {
+        while (count($notKeyedArray) >= 2) {
+            $shifted = (string) array_shift($notKeyedArray);
+            if ($shifted === $key) {
+                return array_shift($notKeyedArray);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @param array<float|int|mixed|string> $notKeyedArray
+     *
+     * @return array<string>
+     */
+    private static function getKeys(array $notKeyedArray): array
+    {
+        $keys = array_filter(array_values($notKeyedArray), static function (int $key): bool {
+            return 0 === $key % 2;
+        }, ARRAY_FILTER_USE_KEY);
+
+        return array_map(static function ($key): string {
+            return (string) $key;
+        }, $keys);
+    }
+}
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
new file mode 100644
index 0000000..a55dee4
--- /dev/null
+++ b/src/Redis/Response/CursorResponse.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+use function count;
+use Iterator;
+use MacFJA\RediSearch\Redis\Command\CursorRead;
+use Predis\ClientInterface;
+use Predis\Response\ResponseInterface;
+
+/**
+ * @implements Iterator<int,AggregateResponseItem[]>
+ */
+class CursorResponse implements ResponseInterface, Iterator
+{
+    /** @var array<AggregateResponseItem> */
+    private $items;
+    /** @var int */
+    private $totalCount;
+    /** @var ClientInterface */
+    private $client;
+    /** @var bool */
+    private $doNext = false;
+    /** @var int */
+    private $cursorId;
+    /** @var int */
+    private $size;
+    /** @var int */
+    private $offset = 0;
+    /** @var string */
+    private $index;
+    /** @var string */
+    private $redisVersion;
+
+    /**
+     * @param array<AggregateResponseItem> $items
+     */
+    public function __construct(int $cursorId, int $totalCount, array $items, int $size, string $index, string $redisVersion = '2.0.0')
+    {
+        $this->totalCount = $totalCount;
+        $this->items = $items;
+        $this->size = $size;
+        $this->index = $index;
+        $this->redisVersion = $redisVersion;
+        $this->cursorId = $cursorId;
+    }
+
+    public function setClient(ClientInterface $client): self
+    {
+        $this->client = $client;
+
+        return $this;
+    }
+
+    /**
+     * @return array<AggregateResponseItem>|array<SearchResponseItem>
+     */
+    public function current()
+    {
+        if ($this->doNext) {
+            $cursorRead = new CursorRead($this->redisVersion);
+            $cursorRead->setIndex($this->index);
+            $cursorRead->setCursorId($this->cursorId);
+            $cursorRead->setCount($this->getPageSize());
+            /** @var CursorResponse $next */
+            $next = $this->client->executeCommand($cursorRead);
+            $this->cursorId = $next->cursorId;
+            $this->offset += count($this->items);
+            $this->items = $next->items;
+            $this->doNext = false;
+            // Don't update totalCount as Cursor always return 0
+        }
+
+        return $this->items;
+    }
+
+    public function next(): void
+    {
+        $this->doNext = true;
+    }
+
+    public function getPageSize(): int
+    {
+        return $this->size;
+    }
+
+    public function getPageCount(): int
+    {
+        return (int) ceil($this->totalCount / $this->size);
+    }
+
+    public function key()
+    {
+        if (0 === $this->size) {
+            return 0;
+        }
+
+        return (int) ceil(($this->offset) / $this->size) + 1;
+    }
+
+    public function valid()
+    {
+        return $this->offset + count($this->items) < $this->totalCount && $this->cursorId > 0;
+    }
+
+    public function rewind(): void
+    {
+        // Do nothing
+    }
+
+    public function getTotalCount(): int
+    {
+        return $this->totalCount;
+    }
+}
diff --git a/src/Redis/Response/InfoResponse.php b/src/Redis/Response/InfoResponse.php
new file mode 100644
index 0000000..2cd23df
--- /dev/null
+++ b/src/Redis/Response/InfoResponse.php
@@ -0,0 +1,242 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+use function assert;
+use BadMethodCallException;
+use function in_array;
+use InvalidArgumentException;
+use function is_array;
+use function is_string;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use Predis\Response\ResponseInterface;
+use Predis\Response\Status;
+
+/**
+ * @method string     getIndexName()
+ * @method array      getIndexOptions()
+ * @method array      getFields()
+ * @method int        getNumDocs()
+ * @method int        getMaxDocId()
+ * @method int        getNumTerms()
+ * @method int        getNumRecords()
+ * @method float      getInvertedSzMb()
+ * @method int        getTotalInvertedIndexBlocks()
+ * @method float      getOffsetVectorsSzMb()
+ * @method float      getDocTableSizeMb()
+ * @method float      getSortableValuesSizeMb()
+ * @method float      getKeyTableSizeMb()
+ * @method null|float getRecordsPerDocAvg()
+ * @method null|float getBytesPerRecordAvg()
+ * @method null|float getOffsetsPerTermAvg()
+ * @method null|float getOffsetBitsPerRecordAvg()
+ * @method bool       getHashIndexingFailures()
+ * @method bool       getIndexing()
+ * @method float      getPercentIndexed()
+ * @method array      getGcStats()
+ * @method array      getCursorStats()
+ * @method array      getStopwordsList()
+ */
+class InfoResponse implements ResponseInterface
+{
+    use ArrayResponseTrait;
+
+    private const TO_FLOAT = ['*_sz_mb', '*_size_mb', '*_avg', 'percent_indexed'];
+    private const TO_INT = ['num_*', 'max_doc_id', 'total_inverted_index_blocks', 'hash_indexing_failures'];
+    private const TO_STRING = ['index_name'];
+    private const TO_BOOLEAN = ['indexing'];
+
+    /**
+     * @var array<array<mixed>|float|int|Status|string>
+     */
+    private $rawLines;
+
+    /**
+     * @param array<array<mixed>|float|int|Status|string> $rawLines
+     */
+    public function __construct($rawLines)
+    {
+        $this->rawLines = $rawLines;
+    }
+
+    /**
+     * @param array<mixed> $arguments
+     *
+     * @return null|float|int|mixed
+     */
+    public function __call(string $name, array $arguments)
+    {
+        if (!(0 === strpos($name, 'get'))) {
+            throw new BadMethodCallException();
+        }
+        $property = substr($name, 3);
+        if (!($property[0] === strtoupper($property[0]))) {
+            throw new BadMethodCallException();
+        }
+        $keyName = $this->pascalCaseToSnakeCase($property);
+        $keys = self::getKeys($this->rawLines);
+        if (!in_array($keyName, $keys, true)) {
+            throw new InvalidArgumentException();
+        }
+
+        $value = self::getValue($this->rawLines, $keyName);
+        if ('-nan' === $value || 'inf' === $value) {
+            return null;
+        }
+
+        return $this->cast($value, $keyName);
+    }
+
+    /**
+     * @return array<CreateCommandFieldOption>
+     */
+    public function getFieldsAsOption(): array
+    {
+        $fields = self::getValue($this->rawLines, 'fields');
+        assert(is_array($fields));
+
+        return array_map(static function (array $field): CreateCommandFieldOption {
+            $fieldName = (string) array_shift($field);
+            array_walk($field, static function (&$item) {
+                if ($item instanceof Status) {
+                    $item = $item->getPayload();
+                }
+
+                return $item;
+            });
+
+            switch (self::getValue($field, 'type')) {
+                case 'GEO':
+                    $object = new GeoFieldOption();
+
+                    break;
+
+                case 'NUMERIC':
+                    $object = new NumericFieldOption();
+
+                    break;
+
+                case 'TAG':
+                    $object = new TagFieldOption();
+                    $object = self::setOptionValue($object, $field, 'SEPARATOR', 'separator');
+
+                    break;
+
+                case 'TEXT':
+                default:
+                    $object = new TextFieldOption();
+                    $object = self::setOptionValue($object, $field, 'WEIGHT', 'weight');
+                    $object = self::setOptionValue($object, $field, 'PHONETIC', 'phonetic');
+                    $object = self::setOptionFlag($object, $field, 'NOSTEM', 'no_stem');
+            }
+
+            assert($object instanceof GroupedOption);
+            $object->setDataOfOption('field', $fieldName);
+            $object = self::setOptionFlag($object, $field, 'NOINDEX', 'no_index');
+            $object = self::setOptionFlag($object, $field, 'SORTABLE', 'sortable');
+
+            return $object;
+        }, $fields);
+    }
+
+    /**
+     * @return null|array<string,mixed>|mixed
+     */
+    public function getIndexDefinition(?string $key = null)
+    {
+        $rawDefinitions = self::getValue($this->rawLines, 'index_definition');
+        assert(is_array($rawDefinitions));
+        $data = self::getPairs($rawDefinitions);
+        if (!is_string($key)) {
+            return $data;
+        }
+
+        return $data[$key] ?? null;
+    }
+
+    private function pascalCaseToSnakeCase(string $pascalCase): string
+    {
+        return strtolower(preg_replace('/([^A-Z\s])([A-Z])/', '$1_$2', $pascalCase) ?? $pascalCase);
+    }
+
+    /**
+     * @param mixed $value
+     *
+     * @return float|int|mixed
+     */
+    private function cast($value, string $key)
+    {
+        foreach (self::TO_FLOAT as $floatKey) {
+            if (fnmatch($floatKey, $key)) {
+                return (float) $value;
+            }
+        }
+        foreach (self::TO_INT as $intKey) {
+            if (fnmatch($intKey, $key)) {
+                return (int) $value;
+            }
+        }
+        foreach (self::TO_STRING as $intKey) {
+            if (fnmatch($intKey, $key)) {
+                return (string) $value;
+            }
+        }
+        foreach (self::TO_BOOLEAN as $intKey) {
+            if (fnmatch($intKey, $key)) {
+                return '1' === (string) $value;
+            }
+        }
+
+        return $value;
+    }
+
+    /**
+     * @param array<float|int|string> $fields
+     */
+    private static function setOptionValue(CreateCommandFieldOption $commandOption, array $fields, string $key, string $optionName): CreateCommandFieldOption
+    {
+        assert($commandOption instanceof TextFieldOption || $commandOption instanceof TagFieldOption);
+        if (in_array($key, $fields, true)) {
+            $commandOption->setDataOfOption($optionName, self::getNextValue($fields, $key));
+        }
+
+        return $commandOption;
+    }
+
+    /**
+     * @param array<float|int|string> $fields
+     */
+    private static function setOptionFlag(CreateCommandFieldOption $commandOption, array $fields, string $key, string $optionName): CreateCommandFieldOption
+    {
+        assert($commandOption instanceof TextFieldOption || $commandOption instanceof TagFieldOption);
+        if (in_array($key, $fields, true)) {
+            $commandOption->setDataOfOption($optionName, true);
+        }
+
+        return $commandOption;
+    }
+}
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
new file mode 100644
index 0000000..a787326
--- /dev/null
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -0,0 +1,140 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+use function count;
+use function is_int;
+use Iterator;
+use MacFJA\RediSearch\Redis\Command\PaginatedCommand;
+use Predis\ClientInterface;
+use Predis\Response\ResponseInterface;
+
+/**
+ * @implements Iterator<int,AggregateResponseItem[]|SearchResponseItem[]>
+ */
+class PaginatedResponse implements ResponseInterface, Iterator
+{
+    /** @var array<AggregateResponseItem>|array<SearchResponseItem> */
+    private $items;
+    /** @var PaginatedCommand */
+    private $lastCommand;
+    /** @var int */
+    private $totalCount;
+    /** @var ClientInterface */
+    private $client;
+
+    /** @var null|int */
+    private $requestedOffset;
+    /** @var null|int */
+    private $requestedSize;
+
+    /**
+     * @param AggregateResponseItem[]|SearchResponseItem[] $items
+     */
+    public function __construct(PaginatedCommand $command, int $totalCount, array $items)
+    {
+        $this->totalCount = $totalCount;
+        $this->items = $items;
+        $this->lastCommand = $command;
+    }
+
+    public function setClient(ClientInterface $client): PaginatedResponse
+    {
+        $this->client = $client;
+
+        return $this;
+    }
+
+    /**
+     * @return array<AggregateResponseItem>|array<SearchResponseItem>
+     */
+    public function current()
+    {
+        if ($this->requestedOffset === ($this->lastCommand->getOffset() ?? 0) && ($this->requestedSize === $this->getPageSize())) {
+            $this->requestedOffset = null;
+            $this->requestedSize = null;
+        }
+        if (is_int($this->requestedOffset) && is_int($this->requestedSize)) {
+            $this->updateWithLimit($this->requestedOffset, $this->requestedSize);
+            $this->requestedOffset = null;
+            $this->requestedSize = null;
+        }
+
+        return $this->items;
+    }
+
+    public function next(): void
+    {
+        $this->requestedOffset = ($this->lastCommand->getOffset() ?? 0) + $this->getPageSize();
+        $this->requestedSize = $this->getPageSize();
+    }
+
+    public function getPageSize(): int
+    {
+        return $this->lastCommand->getSize() ?? count($this->items);
+    }
+
+    public function getPageCount(): int
+    {
+        return (int) ceil($this->totalCount / $this->getPageSize());
+    }
+
+    public function key()
+    {
+        if (0 === $this->getPageSize()) {
+            return 0;
+        }
+
+        return (int) ceil(($this->lastCommand->getOffset() ?? 0) / $this->getPageSize()) + 1;
+    }
+
+    public function valid()
+    {
+        return $this->requestedOffset >= 0
+            && $this->requestedSize > 0
+            && $this->requestedOffset < $this->totalCount;
+    }
+
+    public function rewind(): void
+    {
+        $this->requestedOffset = 0;
+        $this->requestedSize = $this->getPageSize();
+    }
+
+    public function getTotalCount(): int
+    {
+        return $this->totalCount;
+    }
+
+    private function updateWithLimit(int $offset, int $size): void
+    {
+        /** @var PaginatedCommand $nextCommand */
+        $nextCommand = clone $this->lastCommand;
+        $nextCommand->setLimit($offset, $size);
+
+        /** @var PaginatedResponse $paginated */
+        $paginated = $this->client->executeCommand($nextCommand);
+        $this->lastCommand = $nextCommand;
+        $this->totalCount = $paginated->totalCount;
+        $this->items = $paginated->items;
+    }
+}
diff --git a/src/Search/Result.php b/src/Redis/Response/SearchResponseItem.php
similarity index 85%
rename from src/Search/Result.php
rename to src/Redis/Response/SearchResponseItem.php
index 962f8d5..0db7f35 100644
--- a/src/Search/Result.php
+++ b/src/Redis/Response/SearchResponseItem.php
@@ -19,12 +19,12 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search;
+namespace MacFJA\RediSearch\Redis\Response;
 
-use function array_keys;
-use MacFJA\RediSearch\Helper\DataHelper;
-
-class Result
+/**
+ * @codeCoverageIgnore Simple value object
+ */
+class SearchResponseItem
 {
     /** @var string */
     private $hash;
@@ -46,8 +46,6 @@ class Result
      */
     public function __construct(string $hash, array $fields = [], ?float $score = null, ?string $payload = null, ?string $sortKey = null)
     {
-        DataHelper::assertArrayOf(array_keys($fields), 'string');
-        DataHelper::assertArrayOf($fields, 'scalar');
         $this->hash = $hash;
         $this->score = $score;
         $this->payload = $payload;
@@ -78,6 +76,16 @@ public function getFields(): array
         return $this->fields;
     }
 
+    /**
+     * @param null|array<mixed>|bool|float|int|string $default
+     *
+     * @return null|array<mixed>|bool|float|int|string
+     */
+    public function getFieldValue(string $fieldName, $default = null)
+    {
+        return $this->fields[$fieldName] ?? $default;
+    }
+
     public function getSortKey(): ?string
     {
         return $this->sortKey;
diff --git a/src/Words/SpellingResult.php b/src/Redis/Response/SpellCheckResponseItem.php
similarity index 55%
rename from src/Words/SpellingResult.php
rename to src/Redis/Response/SpellCheckResponseItem.php
index 7163150..55df80a 100644
--- a/src/Words/SpellingResult.php
+++ b/src/Redis/Response/SpellCheckResponseItem.php
@@ -19,59 +19,63 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Words;
+namespace MacFJA\RediSearch\Redis\Response;
 
-use function array_keys;
-use function assert;
 use function count;
-use function is_string;
-use function key;
-use MacFJA\RediSearch\Helper\DataHelper;
-use function reset;
-use Throwable;
 
-class SpellingResult
+/**
+ * @codeCoverageIgnore Simple value object
+ */
+class SpellCheckResponseItem
 {
     /** @var string */
-    private $searched;
-
-    /** @var array<string, float> */
-    private $suggestions = [];
+    private $term;
+    /** @var array<string,float> */
+    private $suggestions;
 
     /**
-     * @param array<string, float> $suggestions
-     *
-     * @throws Throwable
+     * @param array<string,float> $suggestions
      */
-    public function __construct(string $searched, array $suggestions)
+    public function __construct(string $term, array $suggestions)
     {
-        DataHelper::assert(count($suggestions) > 0);
-        DataHelper::assertArrayOf(array_keys($suggestions), 'string');
-        DataHelper::assertArrayOf($suggestions, 'double');
-        $this->searched = $searched;
+        $this->term = $term;
         $this->suggestions = $suggestions;
     }
 
-    public function getSearched(): string
+    public function getMisspelled(): string
     {
-        return $this->searched;
+        return $this->term;
     }
 
     /**
      * @return array<string, float>
      */
-    public function getSuggestions(): array
+    public function getSuggestions(bool $normalized = false): array
     {
-        return $this->suggestions;
+        if (false === $normalized) {
+            return $this->suggestions;
+        }
+        $highest = max($this->suggestions);
+
+        if ($highest <= 0) {
+            return [];
+        }
+
+        return array_map(static function (float $score) use ($highest) {
+            return $score / $highest;
+        }, $this->suggestions);
     }
 
-    public function getBestSuggestion(): string
+    public function getTopSuggestion(): ?string
     {
-        assert(count($this->suggestions) > 0);
-        reset($this->suggestions);
-        $best = key($this->suggestions);
-        assert(is_string($best));
+        if (0 === count($this->suggestions)) {
+            return null;
+        }
+
+        $local = $this->suggestions;
+        arsort($local);
+        reset($local);
 
-        return $best;
+        return key($local);
     }
 }
diff --git a/src/Suggestion/Result.php b/src/Redis/Response/SuggestionResponseItem.php
similarity index 93%
rename from src/Suggestion/Result.php
rename to src/Redis/Response/SuggestionResponseItem.php
index 24c110e..e69bf4f 100644
--- a/src/Suggestion/Result.php
+++ b/src/Redis/Response/SuggestionResponseItem.php
@@ -19,12 +19,13 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Suggestion;
+namespace MacFJA\RediSearch\Redis\Response;
 
-class Result
+/**
+ * @codeCoverageIgnore Simple value object
+ */
+class SuggestionResponseItem
 {
-    public const DEFAULT_SCORE = 1.0;
-
     /** @var string */
     private $value;
 
diff --git a/src/Search.php b/src/Search.php
deleted file mode 100644
index a6c9803..0000000
--- a/src/Search.php
+++ /dev/null
@@ -1,541 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch;
-
-use function array_chunk;
-use function array_map;
-use function array_merge;
-use function array_shift;
-use function assert;
-use function count;
-use function in_array;
-use InvalidArgumentException;
-use function is_array;
-use function is_int;
-use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\PaginatedResult;
-use MacFJA\RediSearch\Helper\PipelineItem;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Index\Builder as IndexBuilder;
-use MacFJA\RediSearch\Search\Exception\UnsupportedLanguageException;
-use MacFJA\RediSearch\Search\Filter;
-use MacFJA\RediSearch\Search\GeoFilter;
-use MacFJA\RediSearch\Search\Highlight;
-use MacFJA\RediSearch\Search\Result;
-use MacFJA\RediSearch\Search\Summarize;
-use Predis\Client;
-use function reset;
-use Throwable;
-
-/**
- * @SuppressWarnings(PHPMD.TooManyFields) -- Builder Class
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects) -- Builder Class
- */
-class Search implements Builder, Pipeable
-{
-    public const SORT_ASC = 'ASC';
-
-    public const SORT_DESC = 'DESC';
-
-    private const DEFAULT_OFFSET = 0;
-
-    private const DEFAULT_LIMIT = 10;
-
-    /** @var Client */
-    private $redis;
-
-    /** @var null|string */
-    private $index;
-
-    /** @var null|string */
-    private $query;
-
-    /** @var bool */
-    private $noContent = false;
-
-    /** @var bool */
-    private $verbatim = false;
-
-    /** @var bool */
-    private $noStopWords = false;
-
-    /** @var bool */
-    private $withScores = false;
-
-    /** @var bool */
-    private $withPayloads = false;
-
-    /** @var bool */
-    private $withSortKeys = false;
-
-    /** @var array<Filter> */
-    private $filters = [];
-
-    /** @var null|GeoFilter */
-    private $geoFilter;
-
-    /** @var array<string> */
-    private $inKeys = [];
-
-    /** @var array<string> */
-    private $inFields = [];
-
-    /** @var array<string> */
-    private $returns = [];
-
-    /** @var null|Summarize */
-    private $summarize;
-
-    /** @var null|Highlight */
-    private $highlight;
-
-    /** @var null|int */
-    private $slop;
-
-    /** @var bool */
-    private $inOrder = false;
-
-    /** @var null|string */
-    private $language;
-
-    /** @var null|string */
-    private $expander;
-
-    /** @var null|string */
-    private $scorer;
-
-    /** @var bool */
-    private $explainScore = false;
-
-    /** @var null|string */
-    private $extensionPayload;
-
-    /** @var null|string */
-    private $sortBy;
-
-    /** @var null|string */
-    private $sortDirection;
-
-    /** @var null|int */
-    private $resultOffset;
-
-    /** @var null|int */
-    private $resultLimit;
-
-    public function __construct(Client $redis)
-    {
-        $this->redis = $redis;
-        $this->reset();
-    }
-
-    public function reset(): Builder
-    {
-        $this->index = null;
-        $this->query = null;
-        $this->noContent = false;
-        $this->verbatim = false;
-        $this->noStopWords = false;
-        $this->withScores = false;
-        $this->withPayloads = false;
-        $this->withSortKeys = false;
-        $this->filters = [];
-        $this->geoFilter = null;
-        $this->inKeys = [];
-        $this->inFields = [];
-        $this->returns = [];
-        $this->summarize = null;
-        $this->highlight = null;
-        $this->slop = null;
-        $this->inOrder = false;
-        $this->language = null;
-        $this->expander = null;
-        $this->scorer = null;
-        $this->explainScore = false;
-        $this->extensionPayload = null;
-        $this->sortBy = null;
-        $this->sortDirection = null;
-        $this->resultOffset = null;
-        $this->resultLimit = null;
-
-        return $this;
-    }
-
-    public function withIndex(string $index): Search
-    {
-        $this->index = $index;
-
-        return $this;
-    }
-
-    public function withQuery(string $query): Search
-    {
-        $this->query = $query;
-
-        return $this;
-    }
-
-    public function withNoContent(bool $noContent = true): Search
-    {
-        $this->noContent = $noContent;
-
-        return $this;
-    }
-
-    public function withVerbatim(bool $verbatim = true): Search
-    {
-        $this->verbatim = $verbatim;
-
-        return $this;
-    }
-
-    public function withoutStopWords(bool $noStopWords = true): Search
-    {
-        $this->noStopWords = $noStopWords;
-
-        return $this;
-    }
-
-    public function withScores(bool $withScores = true): Search
-    {
-        $this->withScores = $withScores;
-
-        return $this;
-    }
-
-    public function withPayloads(bool $withPayloads = true): Search
-    {
-        $this->withPayloads = $withPayloads;
-
-        return $this;
-    }
-
-    public function withSortKeys(bool $withSortKeys = true): Search
-    {
-        $this->withSortKeys = $withSortKeys;
-
-        return $this;
-    }
-
-    public function addFilter(string $numericField, float $min, float $max): self
-    {
-        $this->filters[] = new Filter($numericField, $min, $max);
-
-        return $this;
-    }
-
-    public function withGeoFilter(string $fieldName, float $lon, float $lat, int $radius, string $unit): Search
-    {
-        $this->geoFilter = new GeoFilter($fieldName, $lon, $lat, $radius, $unit);
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $inKeys
-     *
-     * @throws Throwable If $inKeys is not an array of strings
-     *
-     * @return $this
-     */
-    public function withInKeys(array $inKeys): Search
-    {
-        DataHelper::assertArrayOf($inKeys, 'string');
-        $this->inKeys = $inKeys;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $inFields
-     *
-     * @throws Throwable If $inFields is not an array of strings
-     *
-     * @return $this
-     */
-    public function withInFields(array $inFields): Search
-    {
-        DataHelper::assertArrayOf($inFields, 'string');
-        $this->inFields = $inFields;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $returns
-     *
-     * @throws Throwable If $returns is not an array of strings
-     *
-     * @return $this
-     */
-    public function withReturns(array $returns): Search
-    {
-        DataHelper::assertArrayOf($returns, 'string');
-        $this->returns = $returns;
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $fields
-     * @param array<string> $fragments
-     *
-     * @return $this
-     */
-    public function withSummarize(array $fields = [], array $fragments = [], ?int $length = null, ?string $separator = null): Search
-    {
-        $this->summarize = new Summarize($fields, $fragments, $length, $separator);
-
-        return $this;
-    }
-
-    /**
-     * @param array<string> $fields
-     *
-     * @return $this
-     */
-    public function withHighlight(array $fields = [], ?string $openTag = null, ?string $closeTag = null): Search
-    {
-        $this->highlight = new Highlight($fields, $openTag, $closeTag);
-
-        return $this;
-    }
-
-    public function withSlop(int $slop = 0): Search
-    {
-        $this->slop = $slop;
-
-        return $this;
-    }
-
-    public function withInOrder(bool $inOrder = true): Search
-    {
-        $this->inOrder = $inOrder;
-
-        return $this;
-    }
-
-    /**
-     * @throws UnsupportedLanguageException
-     *
-     * @return $this
-     */
-    public function withLanguage(string $language): Search
-    {
-        DataHelper::assert(in_array($language, IndexBuilder::SUPPORTED_LANGUAGES, true), UnsupportedLanguageException::class);
-        $this->language = $language;
-
-        return $this;
-    }
-
-    public function withExpander(string $expander): Search
-    {
-        $this->expander = $expander;
-
-        return $this;
-    }
-
-    public function withScorer(string $scorer): Search
-    {
-        $this->scorer = $scorer;
-
-        return $this;
-    }
-
-    public function withExplainScore(bool $explainScore = true): Search
-    {
-        $this->explainScore = $explainScore;
-
-        return $this;
-    }
-
-    public function withExtensionPayload(string $extensionPayload): Search
-    {
-        $this->extensionPayload = $extensionPayload;
-
-        return $this;
-    }
-
-    public function sortBy(string $sortBy): Search
-    {
-        $this->sortBy = $sortBy;
-
-        return $this;
-    }
-
-    public function withSortDirection(string $sortDirection): Search
-    {
-        $this->sortDirection = $sortDirection;
-
-        return $this;
-    }
-
-    public function withResultOffset(int $resultOffset): Search
-    {
-        $this->resultOffset = $resultOffset;
-
-        return $this;
-    }
-
-    public function withResultLimit(int $resultLimit): Search
-    {
-        $this->resultLimit = $resultLimit;
-
-        return $this;
-    }
-
-    /**
-     * @return array<Result>
-     *
-     * @deprecated Use \MacFJA\RediSearch\Search::execute()
-     */
-    public function search(): array
-    {
-        return $this->execute()->getItems();
-    }
-
-    /**
-     * @return PaginatedResult<Result>
-     */
-    public function execute(): PaginatedResult
-    {
-        $request = $this->asPipelineItem();
-        $raw = $this->redis->executeCommand($request->getCommand());
-
-        return $request->transform($raw);
-    }
-
-    /**
-     * @return array<string>|string
-     */
-    public function explainQuery(bool $asArray = false)
-    {
-        return $this->redis->executeRaw([true === $asArray ? 'FT.EXPLAINCLI' : 'FT.EXPLAIN', $this->index, $this->query]);
-    }
-
-    public function asPipelineItem(): PipelineItem
-    {
-        try {
-            return PipelineItem::createFromRaw(
-                $this->buildQuery(),
-                static function ($rawResult, array $context): PaginatedResult {
-                    DataHelper::handleRawResult($rawResult);
-
-                    $totalCount = array_shift($rawResult);
-                    assert(is_int($totalCount));
-
-                    $chunkSize = 2 // Hash + fields
-                        + (($context['scores'] ?? false) ? 1 : 0)
-                        + (($context['payloads'] ?? false) ? 1 : 0)
-                        + (($context['sortKeys'] ?? false) ? 1 : 0);
-
-                    $documents = array_chunk($rawResult, $chunkSize);
-
-                    $items = array_map(function ($document) use ($context) {
-                        $hash = array_shift($document) ?? '';
-                        $score = ($context['scores'] ?? false) ? (float) array_shift($document) : null;
-                        $payload = ($context['payloads'] ?? false) ? array_shift($document) : null;
-                        $sortKey = ($context['sortKeys'] ?? false) ? array_shift($document) : null;
-
-                        $fields = [];
-                        if (false === $context['noContent']) {
-                            if (!(1 === count($document))) {
-                                throw new InvalidArgumentException();
-                            }
-                            $rawData = reset($document);
-                            assert(is_array($rawData));
-                            $fields = RedisHelper::getPairs($rawData);
-                        }
-
-                        return new Result($hash, $fields, $score, $payload, $sortKey);
-                    }, $documents);
-
-                    return new class($totalCount, $items, $context['offset'] ?? self::DEFAULT_OFFSET, $context['limit'] ?? self::DEFAULT_LIMIT) extends PaginatedResult {
-                    };
-                },
-                [
-                    'offset' => $this->resultOffset,
-                    'limit' => $this->resultLimit,
-                    'scores' => $this->withScores,
-                    'payloads' => $this->withPayloads,
-                    'sortKeys' => $this->withSortKeys,
-                    'noContent' => $this->noContent,
-                ]
-            );
-        } finally {
-            $this->reset();
-        }
-    }
-
-    /**
-     * @return array<float|int|string>
-     */
-    private function buildQuery(): array
-    {
-        assert(is_string($this->index));
-        assert(is_string($this->query));
-
-        $query = ['FT.SEARCH', $this->index, $this->query];
-        $query = RedisHelper::buildQueryBoolean($query, [
-            'NOCONTENT' => $this->noContent,
-            'VERBATIM' => $this->verbatim,
-            'NOSTOPWORDS' => $this->noStopWords,
-            'WITHSCORES' => $this->withScores,
-            'WITHPAYLOADS' => $this->withPayloads,
-            'WITHSORTKEYS' => $this->withSortKeys,
-            'EXPLAINSCORE' => $this->explainScore,
-            'INORDER' => $this->inOrder,
-        ]);
-        $query = RedisHelper::buildQueryList($query, [
-            'INKEYS' => $this->inKeys,
-            'INFIELDS' => $this->inFields,
-            'RETURN' => $this->returns,
-        ]);
-        $query = RedisHelper::buildQueryNotNull($query, [
-            'LANGUAGE' => $this->language,
-            'EXPANDER' => $this->expander,
-            'SCORER' => $this->scorer,
-            'PAYLOAD' => $this->extensionPayload,
-        ]);
-        $query = RedisHelper::buildQueryPartial($query,
-            array_merge($this->filters, [$this->geoFilter, $this->summarize, $this->highlight])
-        );
-        if (is_int($this->slop)) {
-            $query[] = 'SLOP';
-            $query[] = $this->slop;
-        }
-        if (is_string($this->sortBy)) {
-            $query[] = 'SORTBY';
-            $query[] = $this->sortBy;
-            if (is_string($this->sortDirection)) {
-                $query[] = $this->sortDirection;
-            }
-        }
-        if (is_int($this->resultOffset) && is_int($this->resultLimit)) {
-            $query[] = 'LIMIT';
-            $query[] = $this->resultOffset;
-            $query[] = $this->resultLimit;
-        }
-
-        return $query;
-    }
-}
diff --git a/src/Search/Exception/NotEnoughTermsException.php b/src/Search/Exception/NotEnoughTermsException.php
deleted file mode 100644
index 056218d..0000000
--- a/src/Search/Exception/NotEnoughTermsException.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search\Exception;
-
-class NotEnoughTermsException extends \MacFJA\RediSearch\Words\Exception\NotEnoughTermsException
-{
-}
diff --git a/src/Search/Exception/UnsupportedLanguageException.php b/src/Search/Exception/UnsupportedLanguageException.php
deleted file mode 100644
index 854fb89..0000000
--- a/src/Search/Exception/UnsupportedLanguageException.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search\Exception;
-
-class UnsupportedLanguageException extends \MacFJA\RediSearch\Index\Builder\Exception\UnsupportedLanguageException
-{
-}
diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php
deleted file mode 100644
index bb9b5c4..0000000
--- a/src/Search/QueryBuilder.php
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search;
-
-use function array_map;
-use function implode;
-use MacFJA\RediSearch\Search\QueryBuilder\AndGroup;
-use MacFJA\RediSearch\Search\QueryBuilder\GroupPartialQuery;
-use MacFJA\RediSearch\Search\QueryBuilder\NumericFacet;
-use MacFJA\RediSearch\Search\QueryBuilder\OrGroup;
-use MacFJA\RediSearch\Search\QueryBuilder\PartialQuery;
-use MacFJA\RediSearch\Search\QueryBuilder\TagFacet;
-use MacFJA\RediSearch\Search\QueryBuilder\TextFacet;
-use MacFJA\RediSearch\Search\QueryBuilder\Word;
-use function strlen;
-
-class QueryBuilder
-{
-    /** @var GroupPartialQuery */
-    private $group;
-
-    /** @var array<QueryBuilder> */
-    private $queryBuilders = [];
-
-    private function __construct(bool $isOrGroup)
-    {
-        if (true === $isOrGroup) {
-            $this->group = new OrGroup();
-
-            return;
-        }
-        $this->group = new AndGroup();
-    }
-
-    public function addTagFacet(string $tagName, string ...$values): self
-    {
-        $this->group->addExpression(
-            new TagFacet($tagName, ...$values)
-        );
-
-        return $this;
-    }
-
-    public function addTextFacet(string $fieldName, string ...$values): self
-    {
-        $this->group->addExpression(
-            new TextFacet($fieldName, ...$values)
-        );
-
-        return $this;
-    }
-
-    public function addNumericFacet(string $fieldName, ?float $min, ?float $max): self
-    {
-        $this->group->addExpression(
-            new NumericFacet($fieldName, $min, $max)
-        );
-
-        return $this;
-    }
-
-    public function addString(string ...$string): self
-    {
-        $this->group->addExpression(new Word(...$string));
-
-        return $this;
-    }
-
-    public function addQueryBuilder(QueryBuilder $queryBuilder): self
-    {
-        $this->queryBuilders[] = $queryBuilder;
-
-        return $this;
-    }
-
-    public function addExpression(PartialQuery $expression): self
-    {
-        $this->group->addExpression($expression);
-
-        return $this;
-    }
-
-    public function render(): string
-    {
-        $otherQueries = implode(' ', array_map(
-            function (QueryBuilder $queryBuilder): string {
-                return $queryBuilder->render();
-            }, $this->queryBuilders
-        ));
-
-        return $this->group->render().(strlen($otherQueries) > 0 ? ' '.$otherQueries : '');
-    }
-
-    public static function createOrGroup(): self
-    {
-        return new self(true);
-    }
-
-    public static function createAndGroup(): self
-    {
-        return new self(false);
-    }
-
-    public static function create(): self
-    {
-        return new self(false);
-    }
-}
diff --git a/src/Search/QueryBuilder/GeoFacet.php b/src/Search/QueryBuilder/GeoFacet.php
deleted file mode 100644
index 7f4a716..0000000
--- a/src/Search/QueryBuilder/GeoFacet.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search\QueryBuilder;
-
-use function in_array;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use MacFJA\RediSearch\Search\Exception\UnknownUnitException;
-use MacFJA\RediSearch\Search\GeoFilter;
-use function sprintf;
-
-class GeoFacet implements PartialQuery
-{
-    /** @var string */
-    private $fieldName;
-
-    /** @var float */
-    private $lon;
-
-    /** @var float */
-    private $lat;
-
-    /** @var int */
-    private $radius;
-
-    /** @var string */
-    private $unit;
-
-    public function __construct(string $fieldName, float $lon, float $lat, int $radius, string $unit)
-    {
-        DataHelper::assert(
-            in_array($unit, [GeoFilter::UNIT_FEET, GeoFilter::UNIT_KILOMETERS, GeoFilter::UNIT_METERS, GeoFilter::UNIT_MILES], true),
-            new UnknownUnitException($unit)
-        );
-        $this->fieldName = $fieldName;
-        $this->lon = $lon;
-        $this->lat = $lat;
-        $this->radius = $radius;
-        $this->unit = $unit;
-    }
-
-    public function render(): string
-    {
-        return sprintf('@%s:[%f %f %f %s]', EscapeHelper::escapeFieldName($this->fieldName), $this->lon, $this->lat, $this->radius, $this->unit);
-    }
-
-    public function includeSpace(): bool
-    {
-        return false;
-    }
-
-    public function priority(): int
-    {
-        return self::PRIORITY_NORMAL;
-    }
-}
diff --git a/src/Search/QueryBuilder/GroupPartialQuery.php b/src/Search/QueryBuilder/GroupPartialQuery.php
deleted file mode 100644
index a79c211..0000000
--- a/src/Search/QueryBuilder/GroupPartialQuery.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search\QueryBuilder;
-
-interface GroupPartialQuery extends PartialQuery
-{
-    public function addExpression(PartialQuery $expression): void;
-}
diff --git a/src/Search/QueryBuilder/TextFacet.php b/src/Search/QueryBuilder/TextFacet.php
deleted file mode 100644
index 71dc2cc..0000000
--- a/src/Search/QueryBuilder/TextFacet.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search\QueryBuilder;
-
-use function array_map;
-use function count;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use MacFJA\RediSearch\Search\Exception\NotEnoughTermsException;
-use function reset;
-use function sprintf;
-use function strpos;
-
-class TextFacet implements PartialQuery
-{
-    private const WITH_SPACE_PATTERN = '@%s:(%s)';
-
-    private const WITHOUT_SPACE_PATTERN = '@%s:%s';
-
-    /** @var string */
-    private $field;
-
-    /** @var array<string> */
-    private $orValues;
-
-    public function __construct(string $field, string ...$orValues)
-    {
-        DataHelper::assert(count($orValues) > 0, NotEnoughTermsException::class);
-        $this->field = $field;
-        $this->orValues = $orValues;
-    }
-
-    public function render(): string
-    {
-        if (count($this->orValues) > 1) {
-            $terms = OrGroup::renderNoParentheses(...array_map(function (string $orValue) {
-                return new ExactMatch($orValue);
-            }, $this->orValues));
-
-            return sprintf(self::WITH_SPACE_PATTERN, EscapeHelper::escapeFieldName($this->field), $terms);
-        }
-
-        $value = reset($this->orValues) ?: '';
-        if (false === strpos($value, ' ')) {
-            return sprintf(self::WITHOUT_SPACE_PATTERN, EscapeHelper::escapeFieldName($this->field), EscapeHelper::escapeWord($value));
-        }
-
-        return sprintf(self::WITH_SPACE_PATTERN, EscapeHelper::escapeFieldName($this->field), EscapeHelper::escapeWord($value));
-    }
-
-    public function includeSpace(): bool
-    {
-        return false;
-    }
-
-    public function priority(): int
-    {
-        return self::PRIORITY_NORMAL;
-    }
-}
diff --git a/src/Search/Summarize.php b/src/Search/Summarize.php
deleted file mode 100644
index f323150..0000000
--- a/src/Search/Summarize.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch\Search;
-
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-
-class Summarize implements \MacFJA\RediSearch\PartialQuery
-{
-    /** @var array<string> */
-    private $fields = [];
-
-    /** @var array<string> */
-    private $fragments = [];
-
-    /** @var null|int */
-    private $length;
-
-    /** @var null|string */
-    private $separator;
-
-    /**
-     * @param array<string> $fields
-     * @param array<string> $fragments
-     */
-    public function __construct(array $fields = [], array $fragments = [], ?int $length = null, ?string $separator = null)
-    {
-        DataHelper::assertArrayOf($fields, 'string');
-        DataHelper::assertArrayOf($fragments, 'string');
-        $this->fields = $fields;
-        $this->fragments = $fragments;
-        $this->length = $length;
-        $this->separator = $separator;
-    }
-
-    public function getQueryParts(): array
-    {
-        $query = ['SUMMARIZE'];
-        $query = RedisHelper::buildQueryList($query, [
-            'FIELDS' => $this->fields,
-            'FRAGS' => $this->fragments,
-        ]);
-
-        return RedisHelper::buildQueryNotNull($query, [
-            'LEN' => $this->length,
-            'SEPARATOR' => $this->separator,
-        ]);
-    }
-}
diff --git a/src/Suggestions.php b/src/Suggestions.php
index 3c232b7..5403d3f 100644
--- a/src/Suggestions.php
+++ b/src/Suggestions.php
@@ -21,106 +21,85 @@
 
 namespace MacFJA\RediSearch;
 
-use function array_chunk;
-use function array_map;
-use function array_shift;
-use function assert;
-use function count;
-use function in_array;
-use InvalidArgumentException;
+use function is_int;
 use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\PipelineItem;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Suggestion\Result;
-use Predis\Client;
+use MacFJA\RediSearch\Redis\Command\SugAdd;
+use MacFJA\RediSearch\Redis\Command\SugDel;
+use MacFJA\RediSearch\Redis\Command\SugGet;
+use MacFJA\RediSearch\Redis\Command\SugLen;
+use MacFJA\RediSearch\Redis\Initializer;
+use MacFJA\RediSearch\Redis\Response\SuggestionResponseItem;
+use Predis\ClientInterface;
 
 /**
- * @property-read int $length
+ * @codeCoverageIgnore
  */
 class Suggestions
 {
     /** @var string */
-    private $dictionaryKey;
-
-    /** @var ?int */
+    private $dictionary;
+    /** @var ClientInterface */
+    private $client;
+    /** @var int */
     private $length;
+    /** @var string */
+    private $version;
 
-    /** @var Client */
-    private $redis;
-
-    public function __construct(string $dictionaryKey, Client $redis)
+    public function __construct(string $dictionary, ClientInterface $client)
     {
-        $this->dictionaryKey = $dictionaryKey;
-        $this->redis = $redis;
+        $this->dictionary = $dictionary;
+        $this->client = $client;
+        $this->length = $client->executeCommand((new SugLen())->setDictionary($dictionary));
+        $this->version = Initializer::getRediSearchVersion($client) ?? '2.0.0';
     }
 
-    /**
-     * @return int|void
-     */
-    public function __get(string $name)
+    public function add(string $suggestion, float $score, bool $increment = false, ?string $payload = null): void
     {
-        if ('length' === $name) {
-            return $this->length ?? $this->length();
+        $command = new SugAdd($this->version);
+        $command
+            ->setDictionary($this->dictionary)
+            ->setScore($score)
+            ->setSuggestion($suggestion)
+            ->setIncrement($increment)
+        ;
+        if (is_string($payload)) {
+            $command->setPayload($payload);
         }
 
-        return;
-    }
-
-    public function __isset(string $name)
-    {
-        return in_array($name, ['length'], true);
-    }
+        $response = $this->client->executeCommand($command);
 
-    public function add(string $suggestion, float $score, bool $increment = false, ?string $payload = null): void
-    {
-        $command = ['FT.SUGADD', $this->dictionaryKey, $suggestion, $score];
-        $command = RedisHelper::buildQueryBoolean($command, ['INCR' => $increment]);
-        $command = RedisHelper::buildQueryNotNull($command, ['PAYLOAD' => $payload]);
-        /** @phan-suppress-next-line PhanAccessReadOnlyMagicProperty */
-        $this->length = $this->redis->executeRaw($command);
+        if (is_int($response)) {
+            $this->length = $response;
+        }
     }
 
     /**
-     * @return array<Result>
+     * @return array<SuggestionResponseItem>
      */
     public function get(string $prefix, bool $fuzzy = false, bool $withScores = false, bool $withPayloads = false, ?int $max = null): array
     {
-        $request = $this->pipeableGet($prefix, $fuzzy, $withScores, $withPayloads, $max);
-        $result = $this->redis->executeCommand($request->getCommand());
-
-        return $request->transform($result);
-    }
-
-    public function pipeableGet(string $prefix, bool $fuzzy = false, bool $withScores = false, bool $withPayloads = false, ?int $max = null): PipelineItem
-    {
-        $command = ['FT.SUGGET', $this->dictionaryKey, $prefix];
-        $command = RedisHelper::buildQueryBoolean($command, [
-            'FUZZY' => $fuzzy,
-            'WITHSCORES' => $withScores,
-            'WITHPAYLOADS' => $withPayloads,
-        ]);
-        $command = RedisHelper::buildQueryNotNull($command, [
-            'MAX' => $max,
-        ]);
-
-        return PipelineItem::createFromRaw(
-            $command,
-            function ($rawResult, array $context) {
-                if (null === $rawResult) {
-                    return [];
-                }
+        $command = new SugGet($this->version);
+        $command->setDictionary($this->dictionary)
+            ->setWithPayloads($withPayloads)
+            ->setWithScores($withScores)
+            ->setPrefix($prefix)
+            ->setFuzzy($fuzzy)
+        ;
+        if (is_int($max)) {
+            $command->setMax($max);
+        }
 
-                return self::listFromRawRedis($rawResult, $context['payloads'], $context['scores']);
-            },
-            ['payloads' => $withPayloads, 'scores' => $withScores]
-        );
+        return $this->client->executeCommand($command);
     }
 
     public function delete(string $suggestion): bool
     {
-        $removed = (int) $this->redis->executeRaw(['FT.SUGDEL', $this->dictionaryKey, $suggestion]);
-        /** @phan-suppress-next-line PhanAccessReadOnlyMagicProperty */
+        $command = new SugDel($this->version);
+        $command
+            ->setDictionary($this->dictionary)
+            ->setSuggestion($suggestion)
+        ;
+        $removed = (int) $this->client->executeCommand($command);
         $this->length -= $removed;
 
         return 1 === $removed;
@@ -128,57 +107,6 @@ public function delete(string $suggestion): bool
 
     public function length(): int
     {
-        /** @phan-suppress-next-line PhanAccessReadOnlyMagicProperty */
-        $this->length = (int) $this->redis->executeRaw(['FT.SUGLEN', $this->dictionaryKey]);
-
         return $this->length;
     }
-
-    /**
-     * @param array<float|string> $data
-     *
-     * @return array<Result>
-     */
-    private static function listFromRawRedis(array $data, bool $withPayload = true, bool $withScore = false): array
-    {
-        $size = 1;
-        if (true === $withPayload) {
-            $size++;
-        }
-        if (true === $withScore) {
-            $size++;
-        }
-        $suggestions = array_chunk($data, $size, false);
-
-        return array_map(function (array $grouped) use ($withPayload, $withScore) {
-            return self::fromRawRedis($grouped, $withPayload, $withScore);
-        }, $suggestions);
-    }
-
-    /**
-     * @param array<float|string> $data
-     */
-    private static function fromRawRedis(array $data, bool $withPayload = true, bool $withScore = false): Result
-    {
-        $expectedSize = 1 + (true === $withPayload ? 1 : 0) + (true === $withScore ? 1 : 0);
-        if (!(count($data) === $expectedSize)) {
-            throw new InvalidArgumentException();
-        }
-
-        $value = array_shift($data);
-        $score = null;
-        $payload = null;
-
-        if (true === $withScore) {
-            $score = (float) array_shift($data);
-        }
-        if (true === $withPayload) {
-            $payload = array_shift($data);
-        }
-
-        DataHelper::assert(is_string($payload) || null === $payload);
-        assert(is_string($payload) || null === $payload);
-
-        return new Result((string) $value, $score, $payload);
-    }
 }
diff --git a/src/Words.php b/src/Words.php
deleted file mode 100644
index 76f2004..0000000
--- a/src/Words.php
+++ /dev/null
@@ -1,206 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace MacFJA\RediSearch;
-
-use function array_chunk;
-use function array_column;
-use function array_combine;
-use function array_map;
-use function array_merge;
-use function assert;
-use function count;
-use function is_array;
-use function is_bool;
-use function is_string;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\PipelineItem;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Words\Exception\NotEnoughTermsException;
-use MacFJA\RediSearch\Words\SpellingResult;
-use MacFJA\RediSearch\Words\SynonymResult;
-use Predis\Client;
-use Throwable;
-
-class Words
-{
-    /** @var Client */
-    private $redis;
-
-    public function __construct(Client $redis)
-    {
-        $this->redis = $redis;
-    }
-
-    /**
-     * @param array<string> $synonyms
-     */
-    public function addSynonym(string $index, string $for, array $synonyms, bool $skipInitialScan = false): bool
-    {
-        DataHelper::assert(count($synonyms) > 0);
-        DataHelper::assertArrayOf($synonyms, 'string');
-        $query = ['FT.SYNUPDATE', $index, $for];
-        $query = RedisHelper::buildQueryBoolean($query, ['SKIPINITIALSCAN' => $skipInitialScan]);
-        $query = array_merge($query, $synonyms);
-
-        return 'OK' === $this->redis->executeRaw($query);
-    }
-
-    /**
-     * @return array<SynonymResult>
-     */
-    public function dumpSynonyms(string $index): array
-    {
-        $result = $this->redis->executeRaw(['FT.SYNDUMP', $index]);
-        DataHelper::handleRawResult($result);
-
-        return array_map(function (array $item): SynonymResult {
-            return new SynonymResult($item[0], $item[1]);
-        }, array_chunk($result, 2));
-    }
-
-    /**
-     * @throws Throwable
-     *
-     * @return array<SpellingResult>
-     */
-    public function getSpellCheck(string $index, string $query, ?int $distance = null): array
-    {
-        return $this->internalSpellCheck($index, $query, $distance);
-    }
-
-    public function getPipeableSpellCheck(string $index, string $query, ?int $distance = null): PipelineItem
-    {
-        $redisQuery = ['FT.SPELLCHECK', $index, $query];
-        $redisQuery = RedisHelper::buildQueryNotNull($redisQuery, ['DISTANCE' => $distance]);
-
-        return PipelineItem::createFromRaw(
-            $redisQuery,
-            function ($result) {
-                return array_map(function (array $term): SpellingResult {
-                    $searched = $term[1];
-                    $suggestions = array_combine(
-                        array_column($term[2], 1),
-                        array_column($term[2], 0)
-                    );
-                    assert(is_array($suggestions));
-
-                    return new SpellingResult($searched, $suggestions);
-                }, $result);
-            }
-        );
-    }
-
-    /**
-     * @param array<string> $terms
-     *
-     * @throws Throwable
-     *
-     * @return array<SpellingResult>
-     */
-    public function getSpellCheckInclude(string $index, string $query, string $dictionary, array $terms = [], ?int $distance = null): array
-    {
-        DataHelper::assertArrayOf($terms, 'string');
-
-        return $this->internalSpellCheck($index, $query, $distance, true, $dictionary, $terms);
-    }
-
-    /**
-     * @param array<string> $terms
-     *
-     * @throws Throwable
-     *
-     * @return array<SpellingResult>
-     */
-    public function getSpellCheckExclude(string $index, string $query, string $dictionary, array $terms = [], ?int $distance = null): array
-    {
-        DataHelper::assertArrayOf($terms, 'string');
-
-        return $this->internalSpellCheck($index, $query, $distance, false, $dictionary, $terms);
-    }
-
-    /**
-     * @param array<string> $terms
-     *
-     * @throws NotEnoughTermsException
-     */
-    public function addToDictionary(string $dictionary, array $terms): int
-    {
-        DataHelper::assert(count($terms) < 1, NotEnoughTermsException::class);
-        $query = array_merge(['FT.DICTADD', $dictionary], $terms);
-
-        return $this->redis->executeRaw($query);
-    }
-
-    /**
-     * @param array<string> $terms
-     *
-     * @throws NotEnoughTermsException
-     */
-    public function removeFromDictionary(string $dictionary, array $terms): int
-    {
-        DataHelper::assert(count($terms) < 1, NotEnoughTermsException::class);
-        DataHelper::assertArrayOf($terms, 'string');
-        $query = array_merge(['FT.DICTDEL', $dictionary], $terms);
-
-        return $this->redis->executeRaw($query);
-    }
-
-    /**
-     * @return array<string>
-     */
-    public function dumpDictionary(string $dictionary): array
-    {
-        return $this->redis->executeRaw(['FT.DICTDUMP', $dictionary]);
-    }
-
-    /**
-     * @param array<string> $terms
-     *
-     * @throws Throwable
-     *
-     * @return array<SpellingResult>
-     */
-    private function internalSpellCheck(string $index, string $query, ?int $distance = null, ?bool $withInclude = null, ?string $dictionary = null, array $terms = []): array
-    {
-        $redisQuery = ['FT.SPELLCHECK', $index, $query];
-        $redisQuery = RedisHelper::buildQueryNotNull($redisQuery, ['DISTANCE' => $distance]);
-        if (is_bool($withInclude) && is_string($dictionary)) {
-            $redisQuery[] = 'TERMS';
-            $redisQuery[] = true === $withInclude ? 'INCLUDE' : 'EXCLUDE';
-            $redisQuery[] = $dictionary;
-            $redisQuery = array_merge($redisQuery, $terms);
-        }
-
-        $result = $this->redis->executeRaw($redisQuery);
-
-        return array_map(function (array $term): SpellingResult {
-            $searched = $term[1];
-            $suggestions = array_combine(
-                array_column($term[2], 1),
-                array_column($term[2], 0)
-            );
-            assert(is_array($suggestions));
-
-            return new SpellingResult($searched, $suggestions);
-        }, $result);
-    }
-}
diff --git a/tests/Aggregate/GroupByTest.php b/tests/Aggregate/GroupByTest.php
deleted file mode 100644
index 57ec052..0000000
--- a/tests/Aggregate/GroupByTest.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Aggregate;
-
-use MacFJA\RediSearch\Aggregate\GroupBy;
-use MacFJA\RediSearch\Aggregate\Reducer;
-use PHPUnit\Framework\TestCase;
-use RuntimeException;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Aggregate\GroupBy
- * @covers ::__construct
- * @covers ::getQueryParts
- *
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- * @uses \MacFJA\RediSearch\Helper\RedisHelper
- * @uses \MacFJA\RediSearch\Aggregate\Reducer
- *
- * @see https://oss.redislabs.com/redisearch/Aggregations/#aggregate_request_format
- */
-class GroupByTest extends TestCase
-{
-    use Assertion;
-
-    public function testNominal(): void
-    {
-        self::assertSameQuery(
-            'GROUPBY 1 @name REDUCE COUNT 0',
-            new GroupBy(['name'], [Reducer::count()])
-        );
-        self::assertSameQuery(
-            'GROUPBY 2 @name @age REDUCE COUNT 0 AS _count REDUCE AVG 1 @age AS _average',
-            new GroupBy(['name', 'age'], [Reducer::count('_count'), Reducer::average('age', '_average')])
-        );
-    }
-
-    public function testNoProperties(): void
-    {
-        self::assertSameQuery(
-            'GROUPBY 0 REDUCE COUNT 0',
-            new GroupBy([], [Reducer::count()])
-        );
-        self::assertSameQuery(
-            'GROUPBY 0 REDUCE COUNT 0 AS _count REDUCE AVG 1 @age AS _average',
-            new GroupBy([], [Reducer::count('_count'), Reducer::average('age', '_average')])
-        );
-    }
-
-    public function testNoReducer(): void
-    {
-        self::assertSameQuery(
-            'GROUPBY 1 @name',
-            new GroupBy(['name'], [])
-        );
-        self::assertSameQuery(
-            'GROUPBY 2 @name @age',
-            new GroupBy(['name', 'age'], [])
-        );
-    }
-
-    public function testInvalidPropertiesDataType(): void
-    {
-        $this->expectException(RuntimeException::class);
-        new GroupBy([1, false], []);
-    }
-
-    public function testInvalidReducerDataType(): void
-    {
-        $this->expectException(RuntimeException::class);
-        new GroupBy([], ['foo', 1, false]);
-    }
-}
diff --git a/tests/Aggregate/ReducerTest.php b/tests/Aggregate/ReducerTest.php
deleted file mode 100644
index 1f7c074..0000000
--- a/tests/Aggregate/ReducerTest.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Aggregate;
-
-use MacFJA\RediSearch\Aggregate\Reducer;
-use PHPUnit\Framework\TestCase;
-use RuntimeException;
-use stdClass;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Aggregate\Reducer
- * @covers ::__construct
- * @covers ::getQueryParts
- *
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- *
- * @see https://oss.redislabs.com/redisearch/Aggregations/#supported_groupby_reducers
- */
-class ReducerTest extends TestCase
-{
-    use Assertion;
-
-    public function testInvalidArgumentsDataType(): void
-    {
-        $this->expectException(RuntimeException::class);
-        new Reducer('foo', [new stdClass()]);
-    }
-
-    /**
-     * @covers ::count
-     */
-    public function testCount(): void
-    {
-        self::assertSameQuery('REDUCE COUNT 0', Reducer::count());
-        self::assertSameQuery('REDUCE COUNT 0 AS countReducer', Reducer::count('countReducer'));
-    }
-
-    /**
-     * @covers ::countDistinct
-     */
-    public function testCountDistinct(): void
-    {
-        self::assertSameQuery('REDUCE COUNT_DISTINCT 1 @foobar', Reducer::countDistinct('foobar'));
-        self::assertSameQuery('REDUCE COUNT_DISTINCT 1 @foobar AS countReducer', Reducer::countDistinct('foobar', 'countReducer'));
-    }
-
-    /**
-     * @covers ::countDistinctish
-     */
-    public function testCountDistinctish(): void
-    {
-        self::assertSameQuery('REDUCE COUNT_DISTINCTISH 1 @foobar', Reducer::countDistinctish('foobar'));
-        self::assertSameQuery('REDUCE COUNT_DISTINCTISH 1 @foobar AS countReducer', Reducer::countDistinctish('foobar', 'countReducer'));
-    }
-
-    /**
-     * @covers ::toList
-     */
-    public function testToList(): void
-    {
-        self::assertSameQuery('REDUCE TOLIST 1 @foobar', Reducer::toList('foobar'));
-        self::assertSameQuery('REDUCE TOLIST 1 @foobar AS list', Reducer::toList('foobar', 'list'));
-    }
-
-    /**
-     * @covers ::median
-     * @covers ::quantile
-     */
-    public function testQuantile(): void
-    {
-        self::assertSameQuery('REDUCE QUANTILE 2 @foobar 0.25', Reducer::quantile('foobar', 0.25));
-        self::assertSameQuery('REDUCE QUANTILE 2 @foobar 0.5 AS median', Reducer::quantile('foobar', 0.5, 'median'));
-        self::assertSameQuery('REDUCE QUANTILE 2 @foobar 0.5 AS median', Reducer::median('foobar', 'median'));
-    }
-
-    /**
-     * @covers ::average
-     */
-    public function testAverage(): void
-    {
-        self::assertSameQuery('REDUCE AVG 1 @foobar', Reducer::average('foobar'));
-        self::assertSameQuery('REDUCE AVG 1 @foobar AS average', Reducer::average('foobar', 'average'));
-    }
-
-    /**
-     * @covers ::maximum
-     */
-    public function testMaximum(): void
-    {
-        self::assertSameQuery('REDUCE MAX 1 @foobar', Reducer::maximum('foobar'));
-        self::assertSameQuery('REDUCE MAX 1 @foobar AS maximum', Reducer::maximum('foobar', 'maximum'));
-    }
-
-    /**
-     * @covers ::minimum
-     */
-    public function testMinimum(): void
-    {
-        self::assertSameQuery('REDUCE MIN 1 @foobar', Reducer::minimum('foobar'));
-        self::assertSameQuery('REDUCE MIN 1 @foobar AS minimum', Reducer::minimum('foobar', 'minimum'));
-    }
-
-    /**
-     * @covers ::sum
-     */
-    public function testSum(): void
-    {
-        self::assertSameQuery('REDUCE SUM 1 @foobar', Reducer::sum('foobar'));
-        self::assertSameQuery('REDUCE SUM 1 @foobar AS sum', Reducer::sum('foobar', 'sum'));
-    }
-
-    /**
-     * @covers ::firstValue
-     */
-    public function testFirstValue(): void
-    {
-        self::assertSameQuery('REDUCE FIRST_VALUE 1 @foobar', Reducer::firstValue('foobar'));
-        self::assertSameQuery('REDUCE FIRST_VALUE 1 @foobar AS first', Reducer::firstValue('foobar', null, null, 'first'));
-        self::assertSameQuery('REDUCE FIRST_VALUE 1 @foobar AS first', Reducer::firstValue('foobar', null, true, 'first'));
-        self::assertSameQuery('REDUCE FIRST_VALUE 3 @foo BY @bar', Reducer::firstValue('foo', 'bar'));
-        self::assertSameQuery('REDUCE FIRST_VALUE 4 @foo BY @bar ASC', Reducer::firstValue('foo', 'bar', true));
-        self::assertSameQuery('REDUCE FIRST_VALUE 4 @foo BY @bar DESC', Reducer::firstValue('foo', 'bar', false));
-        self::assertSameQuery('REDUCE FIRST_VALUE 4 @foo BY @bar DESC AS first', Reducer::firstValue('foo', 'bar', false, 'first'));
-        self::assertSameQuery('REDUCE FIRST_VALUE 3 @foo BY @bar AS first', Reducer::firstValue('foo', 'bar', null, 'first'));
-    }
-
-    /**
-     * @covers ::standardDeviation
-     */
-    public function testStandardDeviation(): void
-    {
-        self::assertSameQuery('REDUCE STDDEV 1 @foobar', Reducer::standardDeviation('foobar'));
-        self::assertSameQuery('REDUCE STDDEV 1 @foobar AS deviation', Reducer::standardDeviation('foobar', 'deviation'));
-    }
-}
diff --git a/tests/Aggregate/SortByTest.php b/tests/Aggregate/SortByTest.php
deleted file mode 100644
index b8a618c..0000000
--- a/tests/Aggregate/SortByTest.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Aggregate;
-
-use MacFJA\RediSearch\Aggregate\Exception\UnknownSortDirectionException;
-use MacFJA\RediSearch\Aggregate\SortBy;
-use OutOfRangeException;
-use PHPUnit\Framework\TestCase;
-use RuntimeException;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Aggregate\SortBy
- * @covers ::__construct
- * @covers ::getQueryParts
- * @covers \MacFJA\RediSearch\Aggregate\Exception\UnknownSortDirectionException
- *
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- * @uses \MacFJA\RediSearch\Helper\RedisHelper
- *
- * @see https://oss.redislabs.com/redisearch/Aggregations/#aggregate_request_format
- */
-class SortByTest extends TestCase
-{
-    use Assertion;
-
-    public function testNominal(): void
-    {
-        self::assertSameQuery(
-            'SORTBY 4 @name ASC @age DESC MAX 20',
-            new SortBy(['name' => SortBy::SORT_ASC, 'age' => SortBy::SORT_DESC], 20)
-        );
-        self::assertSameQuery(
-            'SORTBY 4 @name ASC @age DESC MAX 20',
-            new SortBy(['name' => SortBy::SORT_ASC, '@age' => SortBy::SORT_DESC], 20)
-        );
-        self::assertSameQuery(
-            'SORTBY 2 @name ASC', new SortBy(['name' => SortBy::SORT_ASC]));
-    }
-
-    public function testNoProperties(): void
-    {
-        self::assertSameQuery('SORTBY 0', new SortBy([]));
-        self::assertSameQuery('SORTBY 0 MAX 2', new SortBy([], 2));
-        self::assertSameQuery('SORTBY 0 MAX 0', new SortBy([], 0));
-    }
-
-    public function testInvalidMax(): void
-    {
-        $this->expectException(OutOfRangeException::class);
-        $this->expectExceptionMessage('MAX such by greater or equals to 0');
-        new SortBy([], -1);
-    }
-
-    public function testInvalidSort(): void
-    {
-        $this->expectException(UnknownSortDirectionException::class);
-        $this->expectExceptionMessage('Sort By direction can only be "ASC" or "DESC". Provided: UP, DOWN.');
-        new SortBy(['foo' => 'UP', 'bar' => 'DOWN']);
-    }
-
-    public function testInvalidSortProperties(): void
-    {
-        $this->expectException(RuntimeException::class);
-        new SortBy([1 => 'ASC', false => 'DESC']);
-    }
-}
diff --git a/tests/Helper/DataHelperTest.php b/tests/Helper/DataHelperTest.php
deleted file mode 100644
index 08e9826..0000000
--- a/tests/Helper/DataHelperTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Helper;
-
-use InvalidArgumentException;
-use MacFJA\RediSearch\Helper\DataHelper;
-use PHPUnit\Framework\TestCase;
-use RuntimeException;
-use stdClass;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Helper\DataHelper
- */
-class DataHelperTest extends TestCase
-{
-    /**
-     * @covers ::assert
-     */
-    public function testAssertTrue(): void
-    {
-        DataHelper::assert(true);
-        $this->expectNotToPerformAssertions();
-    }
-
-    /**
-     * @covers ::assert
-     */
-    public function testAssertFalse(): void
-    {
-        $this->expectException(RuntimeException::class);
-        $this->expectExceptionMessage('The assertion of Tests\MacFJA\RediSearch\Helper\DataHelperTest::testAssertFalse at line ');
-
-        DataHelper::assert(false);
-    }
-
-    /**
-     * @covers ::assert
-     */
-    public function testAssertFalseCustomException1(): void
-    {
-        $this->expectException(InvalidArgumentException::class);
-
-        DataHelper::assert(false, InvalidArgumentException::class);
-    }
-
-    /**
-     * @covers ::assert
-     */
-    public function testAssertFalseCustomException2(): void
-    {
-        $exception = new InvalidArgumentException('not valid');
-        $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('not valid');
-
-        DataHelper::assert(false, $exception);
-    }
-
-    /**
-     * @covers ::assert
-     */
-    public function testAssertFalseWrongCustomException(): void
-    {
-        $this->expectException(RuntimeException::class);
-
-        DataHelper::assert(false, stdClass::class);
-    }
-
-    /**
-     * @covers ::assert
-     */
-    public function testAssertEmbed1(): void
-    {
-        $runner = function () {
-            $this->expectException(RuntimeException::class);
-            $this->expectExceptionMessage('The assertion of Tests\MacFJA\RediSearch\Helper\DataHelperTest::Tests\MacFJA\RediSearch\Helper\{closure} at line ');
-            DataHelper::assert(false);
-        };
-        $runner();
-    }
-
-    /**
-     * @covers ::assert
-     */
-    public function testAssertEmbed2(): void
-    {
-        $runner = function () {
-            $this->expectException(RuntimeException::class);
-            $this->expectExceptionMessage('The assertion of Tests\MacFJA\RediSearch\Helper\DataHelperTest::testAssertEmbed2 at line ');
-            DataHelper::assert(false, null, 2);
-        };
-        $runner();
-    }
-}
diff --git a/tests/Helper/EscaperHelperTest.php b/tests/Helper/EscaperHelperTest.php
deleted file mode 100644
index 8637937..0000000
--- a/tests/Helper/EscaperHelperTest.php
+++ /dev/null
@@ -1,175 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Helper;
-
-use function array_combine;
-use function array_map;
-use function fclose;
-use function fgetcsv;
-use function fopen;
-use MacFJA\RediSearch\Helper\EscapeHelper;
-use PHPUnit\Framework\TestCase;
-
-class EscaperHelperTest extends TestCase
-{
-    private $fixtures;
-
-    /**
-     * @covers \MacFJA\RediSearch\Helper\EscapeHelper::escapeFieldName
-     *
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeChar()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCommon()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCharAtWordBegin()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::unescapeNumber()
-     * @dataProvider dataProvider
-     */
-    public function testEscapeFieldName(string $input, string $output): void
-    {
-        if (empty($output)) {
-            self::markTestSkipped('not transformation for '.$input);
-        }
-        self::assertSame($output, EscapeHelper::escapeFieldName($input));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Helper\EscapeHelper::escapeWord
-     *
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeChar()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCommon()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::unescapeNumber()
-     * @dataProvider dataProvider
-     */
-    public function testEscapeWord(string $input, string $output): void
-    {
-        if (empty($output)) {
-            self::markTestSkipped('not transformation for '.$input);
-        }
-        self::assertSame($output, EscapeHelper::escapeWord($input));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Helper\EscapeHelper::escapeFuzzy
-     *
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeChar()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCommon()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::unescapeNumber()
-     *
-     * @dataProvider dataProvider
-     */
-    public function testEscapeFuzzy(string $input, string $output): void
-    {
-        if (empty($output)) {
-            self::markTestSkipped('not transformation for '.$input);
-        }
-        self::assertSame($output, EscapeHelper::escapeFuzzy($input));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Helper\EscapeHelper::escapeExactMatch
-     *
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeChar()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCommon()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::unescapeNumber()
-     *
-     * @dataProvider dataProvider
-     */
-    public function testEscapeExactMatch(string $input, string $output): void
-    {
-        if (empty($output)) {
-            self::markTestSkipped('not transformation for '.$input);
-        }
-        self::assertSame($output, EscapeHelper::escapeExactMatch($input));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Helper\EscapeHelper::escapeNegation
-     *
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeChar()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCommon()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCharAtWordBegin()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::unescapeNumber()
-     *
-     * @dataProvider dataProvider
-     */
-    public function testEscapeNegation(string $input, string $output): void
-    {
-        if (empty($output)) {
-            self::markTestSkipped('not transformation for '.$input);
-        }
-        self::assertSame($output, EscapeHelper::escapeNegation($input));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Helper\EscapeHelper::escapeOptional
-     *
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeChar()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::escapeCommon()
-     * @uses \MacFJA\RediSearch\Helper\EscapeHelper::unescapeNumber()
-     *
-     * @dataProvider dataProvider
-     */
-    public function testEscapeOptional(string $input, string $output): void
-    {
-        if (empty($output)) {
-            self::markTestSkipped('not transformation for '.$input);
-        }
-        self::assertSame($output, EscapeHelper::escapeOptional($input));
-    }
-
-    public function dataProvider(string $testName): array
-    {
-        switch ($testName) {
-            case 'testEscapeFieldName':
-                return $this->getFixture('field name');
-            case 'testEscapeWord':
-                return $this->getFixture('word');
-            case 'testEscapeFuzzy':
-                return $this->getFixture('fuzzy');
-            case 'testEscapeExactMatch':
-                return $this->getFixture('exact match');
-            case 'testEscapeNegation':
-                return $this->getFixture('negation');
-            case 'testEscapeOptional':
-                return $this->getFixture('optional');
-        }
-
-        return [];
-    }
-
-    private function getFixture(string $columnName): array
-    {
-        if (null === $this->fixtures) {
-            $handle = fopen(__DIR__.'/../fixtures/Helper/EscaperHelperFixture.csv', 'rb');
-            $header = fgetcsv($handle);
-            fgetcsv($handle); // Description
-            $this->fixtures = [];
-            while (($row = fgetcsv($handle, 0, ',', '"', '"')) !== false) {
-                $this->fixtures[] = array_combine($header, $row);
-            }
-            fclose($handle);
-        }
-
-        return array_map(function (array $row) use ($columnName) {
-            return [$row['input'], $row[$columnName]];
-        }, $this->fixtures);
-    }
-}
diff --git a/tests/Helper/RedisHelperTest.php b/tests/Helper/RedisHelperTest.php
deleted file mode 100644
index 77e7163..0000000
--- a/tests/Helper/RedisHelperTest.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Helper;
-
-use MacFJA\RediSearch\Helper\RedisHelper;
-use PHPUnit\Framework\TestCase;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Helper\RedisHelper
- */
-class RedisHelperTest extends TestCase
-{
-    /**
-     * @covers ::buildQueryBoolean
-     */
-    public function testBuildQueryBoolean(): void
-    {
-        self::assertSame([], RedisHelper::buildQueryBoolean([], []));
-        self::assertSame([], RedisHelper::buildQueryBoolean([], ['foobar' => false]));
-        self::assertSame(['foobar'], RedisHelper::buildQueryBoolean([], ['foobar' => true]));
-        self::assertSame(['foobar', 'foo'], RedisHelper::buildQueryBoolean([], ['foobar' => true, 'foo' => true, 'bar' => false]));
-        self::assertSame(['foo', 'bar'], RedisHelper::buildQueryBoolean(['foo'], ['bar' => true]));
-    }
-
-    /**
-     * @covers ::buildQueryList
-     */
-    public function testBuildQueryList(): void
-    {
-        self::assertSame([], RedisHelper::buildQueryList([], []));
-        self::assertSame([], RedisHelper::buildQueryList([], ['foobar' => []]));
-        self::assertSame(['foobar', 2, 'foo', 'bar'], RedisHelper::buildQueryList([], ['foobar' => ['foo', 'bar']]));
-        self::assertSame(['foobar', 0], RedisHelper::buildQueryList([], ['foobar' => []], true));
-        self::assertSame(['foo', 2, 'foo', 'bar', 'bar', 1, 'foobar'], RedisHelper::buildQueryList([], ['foo' => ['foo', 'bar'], 'bar' => ['foobar']]));
-        self::assertSame(['foobar', 2, 2, 'bar'], RedisHelper::buildQueryList([], ['foobar' => [2, 'bar']]));
-        self::assertSame(['foo', 'foobar', 2, 2, 'bar'], RedisHelper::buildQueryList(['foo'], ['foobar' => [2, 'bar']]));
-    }
-
-    /**
-     * @covers ::buildQueryNotNull
-     */
-    public function testBuildQueryNotNull(): void
-    {
-        self::assertSame([], RedisHelper::buildQueryNotNull([], []));
-        self::assertSame([], RedisHelper::buildQueryNotNull([], ['foobar' => null]));
-        self::assertSame(['foobar', 2], RedisHelper::buildQueryNotNull([], ['foobar' => 2]));
-        self::assertSame(['foo', 'bar'], RedisHelper::buildQueryNotNull([], ['foo' => 'bar']));
-        self::assertSame(['foo', 'bar', 'hello', 'world'], RedisHelper::buildQueryNotNull([], ['foo' => 'bar', 'hello' => 'world']));
-        self::assertSame(['foobar', 'foo', 'bar'], RedisHelper::buildQueryNotNull(['foobar'], ['foo' => 'bar']));
-    }
-
-    /**
-     * @covers ::getPairs
-     */
-    public function testGetPairs(): void
-    {
-        self::assertSame(['key1' => 'value1', 'key2' => 'value2'], RedisHelper::getPairs(['key1', 'value1', 'key2', 'value2']));
-        self::assertSame(['key1' => 'value1'], RedisHelper::getPairs(['key1', 'value1', 'key2']));
-    }
-
-    /**
-     * @covers ::getValue
-     */
-    public function testGetValue(): void
-    {
-        self::assertSame('value1', RedisHelper::getValue(['key1', 'value1', 'key2', 'value2'], 'key1'));
-        self::assertSame('value2', RedisHelper::getValue(['key1', 'value1', 'key2', 'value2'], 'key2'));
-        self::assertNull(RedisHelper::getValue(['key1', 'value1', 'key2', 'value2'], 'key3'));
-        self::assertSame('value1', RedisHelper::getValue(['key1', 'value1', 'key2'], 'key1'));
-        self::assertNull(RedisHelper::getValue(['key1', 'value1', 'key2'], 'key2'));
-        self::assertSame('value', RedisHelper::getValue(['foo', 'bar', 'foobar', 'key', 'value'], 'key'));
-    }
-}
diff --git a/tests/Index/Builder/TagFieldTest.php b/tests/Index/Builder/TagFieldTest.php
deleted file mode 100644
index c81e231..0000000
--- a/tests/Index/Builder/TagFieldTest.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Index\Builder;
-
-use MacFJA\RediSearch\Index\Builder\Exception\MustBeSingleCharException;
-use MacFJA\RediSearch\Index\Builder\TagField;
-use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @covers \MacFJA\RediSearch\Index\Builder\TagField
- *
- * @uses \MacFJA\RediSearch\Index\Builder\AbstractField
- * @uses \MacFJA\RediSearch\Helper\RedisHelper
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- */
-class TagFieldTest extends TestCase
-{
-    use Assertion;
-
-    public function testNominal(): void
-    {
-        self::assertSameQuery(
-            'genres TAG SORTABLE',
-            new TagField('genres', null, true)
-        );
-        self::assertSameQuery(
-            'keywords TAG SEPARATOR +',
-            new TagField('keywords', '+')
-        );
-        self::assertSameQuery(
-            'ean TAG SEPARATOR % NOINDEX',
-            new TagField('ean', '%', false, true)
-        );
-    }
-
-    public function testGetType(): void
-    {
-        self::assertSame('TAG', (new TagField('foobar'))->getType());
-    }
-
-    public function testWrongSeparator(): void
-    {
-        $this->expectException(MustBeSingleCharException::class);
-        $this->expectExceptionMessage('Separator must be a single char');
-
-        new TagField('foobar', '||');
-    }
-
-    public function testGetSeparator(): void
-    {
-        $tag = new TagField('foobar', '|');
-        self::assertSame('|', $tag->getSeparator());
-
-        $tag = new TagField('foobar');
-        self::assertNull($tag->getSeparator());
-    }
-}
diff --git a/tests/Index/Builder/TextFieldTest.php b/tests/Index/Builder/TextFieldTest.php
deleted file mode 100644
index 31a502f..0000000
--- a/tests/Index/Builder/TextFieldTest.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Index\Builder;
-
-use MacFJA\RediSearch\Index\Builder\TextField;
-use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @covers \MacFJA\RediSearch\Index\Builder\TextField
- *
- * @uses \MacFJA\RediSearch\Index\Builder\AbstractField
- * @uses \MacFJA\RediSearch\Helper\RedisHelper
- */
-class TextFieldTest extends TestCase
-{
-    use Assertion;
-
-    public function testNominal(): void
-    {
-        self::assertSameQuery(
-            'title TEXT WEIGHT 2 PHONETIC dm:en SORTABLE',
-            new TextField('title', false, 2, 'dm:en', true, false)
-        );
-        self::assertSameQuery(
-            'name TEXT NOSTEM NOINDEX',
-            new TextField('name', true, null, null, false, true)
-        );
-        self::assertSameQuery(
-            'code TEXT NOSTEM WEIGHT 4.5',
-            new TextField('code', true, 4.5)
-        );
-    }
-
-    public function testGetType(): void
-    {
-        self::assertSame('TEXT', (new TextField('foobar'))->getType());
-    }
-
-    public function testIsNoStem(): void
-    {
-        $text = new TextField('foobar', false);
-        self::assertFalse($text->isNoStem());
-
-        $text = new TextField('foobar');
-        self::assertFalse($text->isNoStem());
-
-        $text = new TextField('foobar', true);
-        self::assertTrue($text->isNoStem());
-    }
-
-    public function testGetWeight(): void
-    {
-        $text = new TextField('foobar', false, 1);
-        self::assertSame(1.0, $text->getWeight());
-
-        $text = new TextField('foobar', false, null);
-        self::assertNull($text->getWeight());
-
-        $text = new TextField('foobar');
-        self::assertNull($text->getWeight());
-    }
-
-    public function testGetPhonetic(): void
-    {
-        $text = new TextField('foobar', false, null, 'dm:en');
-        self::assertSame('dm:en', $text->getPhonetic());
-
-        $text = new TextField('foobar', false, null, null);
-        self::assertNull($text->getPhonetic());
-
-        $text = new TextField('foobar');
-        self::assertNull($text->getPhonetic());
-    }
-}
diff --git a/src/Aggregate/GroupBy.php b/tests/Query/Builder/FuzzyTest.php
similarity index 52%
rename from src/Aggregate/GroupBy.php
rename to tests/Query/Builder/FuzzyTest.php
index 0fe1a5a..a8af50c 100644
--- a/src/Aggregate/GroupBy.php
+++ b/tests/Query/Builder/FuzzyTest.php
@@ -19,44 +19,39 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Aggregate;
+namespace MacFJA\RediSearch\tests\Query\Builder;
 
-use function array_map;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\PartialQuery;
-use Throwable;
+use MacFJA\RediSearch\Exception\OutOfRangeLevenshteinDistanceException;
+use MacFJA\RediSearch\Query\Builder\Fuzzy;
 
-class GroupBy implements PartialQuery
+/**
+ * @covers \MacFJA\RediSearch\Exception\OutOfRangeLevenshteinDistanceException
+ * @covers \MacFJA\RediSearch\Query\Builder\Fuzzy
+ *
+ * @internal
+ */
+class FuzzyTest extends \PHPUnit\Framework\TestCase
 {
-    /** @var array<string> */
-    private $properties = [];
-
-    /** @var array<Reducer> */
-    private $reducers = [];
-
-    /**
-     * @param array<string>  $properties
-     * @param array<Reducer> $reducers
-     *
-     * @throws Throwable
-     */
-    public function __construct(array $properties, array $reducers)
+    public function testTooLow(): void
     {
-        DataHelper::assertArrayOf($properties, 'string');
-        DataHelper::assertArrayOf($reducers, Reducer::class);
+        $this->expectException(OutOfRangeLevenshteinDistanceException::class);
+        $this->expectExceptionMessage('The Levenshtein distance should be between 1 and 3 (0 provided)');
+        new Fuzzy('hello', 0);
+    }
 
-        $this->properties = $properties;
-        $this->reducers = $reducers;
+    public function testTooHigh(): void
+    {
+        $this->expectException(OutOfRangeLevenshteinDistanceException::class);
+        $this->expectExceptionMessage('The Levenshtein distance should be between 1 and 3 (4 provided)');
+        new Fuzzy('hello', 4);
     }
 
-    public function getQueryParts(): array
+    public function testSpace(): void
     {
-        $atProperties = array_map(static function (string $property) {
-            return '@'.$property;
-        }, $this->properties);
-        $query = RedisHelper::buildQueryList([], ['GROUPBY' => $atProperties], true);
+        $element = new Fuzzy('hello');
+        static::assertFalse($element->includeSpace());
 
-        return RedisHelper::buildQueryPartial($query, $this->reducers);
+        $element = new Fuzzy('hello world');
+        static::assertTrue($element->includeSpace());
     }
 }
diff --git a/tests/Query/Builder/GeoFacetTest.php b/tests/Query/Builder/GeoFacetTest.php
new file mode 100644
index 0000000..7d4877c
--- /dev/null
+++ b/tests/Query/Builder/GeoFacetTest.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query\Builder;
+
+use MacFJA\RediSearch\Exception\UnknownUnitException;
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+
+/**
+ * @covers \MacFJA\RediSearch\Exception\UnknownUnitException
+ * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
+ *
+ * @internal
+ */
+class GeoFacetTest extends \PHPUnit\Framework\TestCase
+{
+    public function testWrongUnit(): void
+    {
+        $this->expectException(UnknownUnitException::class);
+        $this->expectExceptionMessage('The unit "foo" is not valid.');
+
+        new GeoFacet(['foo'], 1.0, 1.0, 1.0, 'foo');
+    }
+}
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
new file mode 100644
index 0000000..6523b8e
--- /dev/null
+++ b/tests/Query/BuilderTest.php
@@ -0,0 +1,508 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query;
+
+use MacFJA\RediSearch\Query\Builder;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Query\Builder
+ * @covers \MacFJA\RediSearch\Query\Builder\AbstractGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\AndGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\ExactMatch
+ * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Fuzzy
+ * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Negation
+ * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Optional
+ * @covers \MacFJA\RediSearch\Query\Builder\OrGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\Prefix
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ * @covers \MacFJA\RediSearch\Query\Builder\RawElement
+ * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Word
+ * @covers \MacFJA\RediSearch\Query\Escaper
+ *
+ * @internal
+ */
+class BuilderTest extends TestCase
+{
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#numeric_filters_in_query
+     */
+    public function testDocNumericExample1(): void
+    {
+        $expected = '@price:[100 200]';
+
+        $builder = new Builder();
+
+        $builder->addNumericFacet('price', 100, 200);
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#numeric_filters_in_query
+     */
+    public function testDocNumericExample2(): void
+    {
+        $expected = '@title:foo -@price:[100 100]';
+
+        $builder = new Builder();
+
+        $builder
+            ->addTextFacet('title', 'foo')
+            ->addElement(
+                new Builder\Negation(
+                    Builder\NumericFacet::equalsTo('price', 100)
+                )
+            )
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * https://oss.redislabs.com/redisearch/Query_Syntax/#pure_negative_queries.
+     */
+    public function testDocNegativeExample1(): void
+    {
+        $expected = '-hello';
+        $builder = new Builder();
+
+        $builder->addElement(new Builder\Negation(new Builder\Word('hello')));
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#field_modifiers
+     */
+    public function testDocModifierExample1(): void
+    {
+        $expected = '@country:korea @engine:(diesel|hybrid) @class:suv';
+        $builder = new Builder();
+
+        $builder->addTextFacet('country', 'korea')
+            ->addTextFacet('engine', 'diesel', 'hybrid')
+            ->addTextFacet('class', 'suv')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#field_modifiers
+     */
+    public function testDocModifierExample2(): void
+    {
+        $expected = '@title|body:(hello world) @url|image:mydomain';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(
+                new Builder\FieldFacet(
+                    ['title', 'body'],
+                    Builder\EncapsulationGroup::simple(Builder\AndGroup::fromStrings('hello', 'world'))
+                )
+            )
+            ->addElement(new Builder\TextFacet(
+                ['url', 'image'],
+                'mydomain'
+            ))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#tag_filters
+     */
+    public function testDocTagExample1(): void
+    {
+        $expected = '@cities:{ New York | Los Angeles | Barcelona }';
+        $expected = str_replace(['{ ', ' | ', ' }'], ['{', '|', '}'], $expected);
+        $builder = new Builder();
+
+        $builder
+            ->addTagFacet('cities', 'New York', 'Los Angeles', 'Barcelona')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#tag_filters
+     */
+    public function testDocTagExample2(): void
+    {
+        $expected = '@cities:{ New York } @cities:{Los Angeles} @cities:{ Barcelona }';
+        $expected = str_replace(['{ ', ' | ', ' }'], ['{', '|', '}'], $expected);
+        $builder = new Builder();
+
+        $builder
+            ->addTagFacet('cities', 'New York')
+            ->addTagFacet('cities', 'Los Angeles')
+            ->addTagFacet('cities', 'Barcelona')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#geo_filters_in_query
+     */
+    public function testDocGeoExample1(): void
+    {
+        $expected = 'chinese restaurant @location:[-122.41 37.77 5 km]';
+        $builder = new Builder();
+
+        $builder
+            ->addString('chinese')
+            ->addString('restaurant')
+            ->addGeoFacet('location', -122.41, 37.77, 5, 'km')
+        ;
+
+        static::assertSame($expected, str_replace(
+            ['.000000 ', '0000 '],
+            [' ', ' '],
+            $builder->render()
+        ));
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#fuzzy_matching
+     */
+    public function testDocFuzzyExample1(): void
+    {
+        $expected = '%hello% world';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(new Builder\Fuzzy('hello'))
+            ->addString('world')
+            ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#fuzzy_matching
+     */
+    public function testDocFuzzyExample2(): void
+    {
+        $expected = '%%hello%%';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(new Builder\Fuzzy('hello', 2))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#query_attributes
+     */
+    public function testDocAttributeExample1(): void
+    {
+        $expected = '(foo bar) => { $weight: 2.0; $slop: 1; $inorder: true; }';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(
+                new Builder\QueryElementAttribute(
+                    Builder\AndGroup::fromStrings('foo', 'bar'),
+                    2.0,
+                    1,
+                    true
+                )
+            )
+        ;
+
+        static::assertSame($expected, str_replace(
+            ['00000; ', ' })', '(('],
+            ['; ', ' }', '('],
+            $builder->render()
+        ));
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#query_attributes
+     */
+    public function testDocAttributeExample2(): void
+    {
+        $expected = '~(bar baz) => { $weight: 0.5; }';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(
+                new Builder\QueryElementAttribute(
+                    new Builder\Optional(Builder\AndGroup::fromStrings('bar', 'baz')),
+                    0.5
+                )
+            )
+        ;
+
+        static::assertSame($expected, str_replace(
+            ['00000; ', ' })', '(~('],
+            ['; ', ' }', '~('],
+            $builder->render()
+        ));
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#query_attributes
+     */
+    public function testDocAttributeExample2bis(): void
+    {
+        $expected = '~(bar baz) => { $weight: 0.5; }';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(
+                new Builder\Optional(
+                    new Builder\QueryElementAttribute(
+                        Builder\AndGroup::fromStrings('foo', 'bar'),
+                        0.5
+                    )
+                )
+            )
+        ;
+
+        static::assertNotSame($expected, str_replace(
+            ['00000; ', ' })', '(~('],
+            ['; ', ' }', '~('],
+            $builder->render()
+        ));
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample1(): void
+    {
+        $expected = 'hello world';
+        $builder = new Builder();
+
+        $builder
+            ->addString('hello')
+            ->addString('world')
+        ;
+
+        static::assertSame($expected, $builder->render());
+
+        $builder = new Builder();
+
+        $builder
+            ->addElement(Builder\AndGroup::fromStrings('hello', 'world'))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample2(): void
+    {
+        $expected = '"hello world"';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(new Builder\ExactMatch('hello world'))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample3(): void
+    {
+        $expected = 'hello|world';
+        $builder = new Builder();
+
+        $builder->addElement(Builder\OrGroup::fromStrings('hello', 'world'))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample4(): void
+    {
+        $expected = 'hello -world';
+        $builder = new Builder();
+
+        $builder
+            ->addString('hello')
+            ->addElement(new Builder\Negation(new Builder\Word('world')))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample5(): void
+    {
+        $expected = '(hello|halo) (world|werld)';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(Builder\OrGroup::fromStrings('hello', 'halo'))
+            ->addElement(Builder\OrGroup::fromStrings('world', 'werld'))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample6(): void
+    {
+        $expected = 'hello -(world|werld)';
+        $builder = new Builder();
+
+        $builder
+            ->addString('hello')
+            ->addElement(
+                new Builder\Negation(
+                    new Builder\EncapsulationGroup(
+                        '(',
+                        ')',
+                        Builder\OrGroup::fromStrings('world', 'werld')
+                    )
+                )
+            )
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample7(): void
+    {
+        $expected = '(barack|barrack) obama';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(Builder\OrGroup::fromStrings('barack', 'barrack'))
+            ->addString('obama')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample8(): void
+    {
+        $expected = 'obama ~barack ~michelle';
+        $builder = new Builder();
+
+        $builder
+            ->addString('obama')
+            ->addElement(new Builder\Optional(new Builder\Word('barack')))
+            ->addElement(new Builder\Optional(new Builder\Word('michelle')))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample9(): void
+    {
+        $expected = '@title:"barack obama" @job:president';
+        $builder = new Builder();
+
+        $builder
+            ->addTextFacet('title', 'barack obama')
+            ->addTextFacet('job', 'president')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample10(): void
+    {
+        $expected = '@title:hello world @body:(foo bar) @category:(articles|biographies)';
+        $builder = new Builder();
+
+        $builder
+            ->addTextFacet('title', 'hello')
+            ->addString('world')
+            ->addElement(new Builder\FieldFacet(['body'], Builder\AndGroup::fromStrings('foo', 'bar')))
+            ->addTextFacet('category', 'articles', 'biographies')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample12(): void
+    {
+        $expected = '@name:tv @price:[200 500]';
+        $builder = new Builder();
+
+        $builder
+            ->addTextFacet('name', 'tv')
+            ->addNumericFacet('price', 200, 500)
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
+    /**
+     * @see https://oss.redislabs.com/redisearch/Query_Syntax/#a_few_query_examples
+     */
+    public function testDocExample13(): void
+    {
+        $expected = '@age:[(18 +inf]';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(Builder\NumericFacet::greaterThan('age', 18))
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+}
diff --git a/tests/Redis/Command/AbstractCommandTest.php b/tests/Redis/Command/AbstractCommandTest.php
new file mode 100644
index 0000000..02ccf69
--- /dev/null
+++ b/tests/Redis/Command/AbstractCommandTest.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use InvalidArgumentException;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakeAbstractCommand;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class AbstractCommandTest extends \PHPUnit\Framework\TestCase
+{
+    public function testGetRediSearchVersion(): void
+    {
+        $command = new FakeAbstractCommand([]);
+
+        static::assertSame('2.0.0', $command->getRediSearchVersion());
+        $command->setRediSearchVersion('2.2.0');
+        static::assertSame('2.2.0', $command->getRediSearchVersion());
+    }
+
+    public function testMissingRequirements(): void
+    {
+        $command = new FakeAbstractCommand(['req' => new NamelessOption()]);
+        $command->setRequiredOptions(['req']);
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('Missing command option');
+
+        $command->getArguments();
+    }
+
+    public function testWrongOptionType(): void
+    {
+        $command = new FakeAbstractCommand(['req' => 10]);
+        $command->setRequiredOptions(['req']);
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('Missing command option');
+
+        $command->getArguments();
+    }
+}
diff --git a/tests/Redis/Command/AddFieldOptionTraitTest.php b/tests/Redis/Command/AddFieldOptionTraitTest.php
new file mode 100644
index 0000000..1f09c57
--- /dev/null
+++ b/tests/Redis/Command/AddFieldOptionTraitTest.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use BadMethodCallException;
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakeAddFieldOptionTraitClass1;
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakeAddFieldOptionTraitClass2;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class AddFieldOptionTraitTest extends \PHPUnit\Framework\TestCase
+{
+    public function testWrongParent(): void
+    {
+        $command = new FakeAddFieldOptionTraitClass1();
+
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('This method is not callable in '.FakeAddFieldOptionTraitClass1::class);
+
+        $command->addNumericField('foo');
+    }
+
+    public function testValidParent(): void
+    {
+        $command = new FakeAddFieldOptionTraitClass2([]);
+
+        $command->addNumericField('foo');
+
+        static::assertTrue(true);
+    }
+}
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
new file mode 100644
index 0000000..d3873d7
--- /dev/null
+++ b/tests/Redis/Command/AggregateTest.php
@@ -0,0 +1,148 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\LimitOption
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ *
+ * @internal
+ */
+class AggregateTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new Aggregate();
+        static::assertSame('FT.AGGREGATE', $command->getId());
+    }
+
+    public function testMultiReduce(): void
+    {
+        $group = new GroupByOption(['@text1']);
+        $group
+            ->addReducer(ReduceOption::countDistinct('user_id'))
+            ->addReducer(ReduceOption::average('num1', 'average'))
+        ;
+        $command = new Aggregate();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->addGroupBy($group)
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'GROUPBY', 1, '@text1', 'REDUCE', 'COUNT_DISTINCT', 1, '@user_id', 'REDUCE', 'AVG', 1, '@num1', 'AS', 'average',
+        ], $command->getArguments());
+    }
+
+    public function testWithCursor(): void
+    {
+        $command = new Aggregate();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setWithCursor(20, 30)
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'WITHCURSOR', 'COUNT', 20, 'MAXIDLE', 30,
+        ], $command->getArguments());
+
+        $command->setWithoutCursor();
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+        ], $command->getArguments());
+    }
+
+    public function testSizeOptions(): void
+    {
+        $command = new Aggregate();
+        static::assertNull($command->getSize());
+        static::assertNull($command->getOffset());
+
+        $command->setLimit(0, 10);
+        static::assertSame(0, $command->getOffset());
+        static::assertSame(10, $command->getSize());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new Aggregate();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setVerbatim()
+            ->setLoad('@geo1', '@tag1')
+            ->addGroupBy(new GroupByOption(['@text1'], [ReduceOption::countDistinct('user_id'), ReduceOption::average('num1', 'average')]))
+            ->addFilter("@name=='foo' && @age < 20")
+            ->addSortBy('@text1')
+            ->addSortBy('@num1', 'DESC')
+            ->setSortByMax(10)
+            ->setLimit(12, 35)
+            ->addApply('@timestamp - (@timestamp % 86400)', 'day')
+            ->setWithCursor(20, 30)
+            ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'VERBATIM',
+            'LOAD', 2, '@geo1', '@tag1',
+            'GROUPBY', 1, '@text1', 'REDUCE', 'COUNT_DISTINCT', 1, '@user_id', 'REDUCE', 'AVG', 1, '@num1', 'AS', 'average',
+            'SORTBY', 3, '@text1', '@num1', 'DESC', 'MAX', 10,
+            'APPLY', '@timestamp - (@timestamp % 86400)',  'AS', 'day',
+            'LIMIT', 12, 35,
+            'FILTER', "@name=='foo' && @age < 20",
+            'WITHCURSOR', 'COUNT', 20, 'MAXIDLE', 30,
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php b/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php
new file mode 100644
index 0000000..50ba656
--- /dev/null
+++ b/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\AggregationCommand;
+
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class ReduceOptionTest extends \PHPUnit\Framework\TestCase
+{
+    public function testShorthands(): void
+    {
+        static::assertSame(['REDUCE', 'COUNT', 0, 'AS', 'count'], ReduceOption::count('count')->render());
+        static::assertSame(['REDUCE', 'COUNT_DISTINCT', 1, '@name', 'AS', 'distinct'], ReduceOption::countDistinct('name', 'distinct')->render());
+        static::assertSame(['REDUCE', 'COUNT_DISTINCTISH', 1, '@city', 'AS', 'cities_count'], ReduceOption::countDistinctish('city', 'cities_count')->render());
+        static::assertSame(['REDUCE', 'SUM', 1, '@delta', 'AS', 'total'], ReduceOption::sum('delta', 'total')->render());
+        static::assertSame(['REDUCE', 'MIN', 1, '@distance', 'AS', 'shortest'], ReduceOption::minimum('distance', 'shortest')->render());
+        static::assertSame(['REDUCE', 'MAX', 1, '@distance', 'AS', 'longest'], ReduceOption::maximum('distance', 'longest')->render());
+        static::assertSame(['REDUCE', 'AVG', 1, '@age', 'AS', 'avg'], ReduceOption::average('age', 'avg')->render());
+        static::assertSame(['REDUCE', 'STDDEV', 1, '@fare', 'AS', 'deviation'], ReduceOption::standardDeviation('fare', 'deviation')->render());
+        static::assertSame(['REDUCE', 'QUANTILE', 2, '@num', 0.25, 'AS', 'quarter'], ReduceOption::quantile('num', 0.25, 'quarter')->render());
+        static::assertSame(['REDUCE', 'QUANTILE', 2, '@num', 0.5, 'AS', 'median'], ReduceOption::median('num', 'median')->render());
+        static::assertSame(['REDUCE', 'TOLIST', 1, '@year', 'AS', 'years'], ReduceOption::toList('year', 'years')->render());
+        static::assertSame(['REDUCE', 'FIRST_VALUE', 4, '@name', 'BY', '@age', 'DESC', 'AS', 'n'], ReduceOption::firstValue('name', 'age', false, 'n')->render());
+    }
+
+    public function testGetOptionData(): void
+    {
+        static::assertSame([
+            'function' => 'FIRST_VALUE',
+            'arguments' => ['@name', 'BY', '@age', 'DESC'],
+            'alias' => 'n',
+        ], ReduceOption::firstValue('name', 'age', false, 'n')->getOptionData());
+    }
+}
diff --git a/tests/Index/Builder/NumericFieldTest.php b/tests/Redis/Command/AliasAddTest.php
similarity index 59%
rename from tests/Index/Builder/NumericFieldTest.php
rename to tests/Redis/Command/AliasAddTest.php
index 42cce84..12b07d2 100644
--- a/tests/Index/Builder/NumericFieldTest.php
+++ b/tests/Redis/Command/AliasAddTest.php
@@ -19,31 +19,39 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace Tests\MacFJA\RediSearch\Index\Builder;
+namespace MacFJA\RediSearch\tests\Redis\Command;
 
-use MacFJA\RediSearch\Index\Builder\NumericField;
+use MacFJA\RediSearch\Redis\Command\AliasAdd;
 use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
 
 /**
- * @covers \MacFJA\RediSearch\Index\Builder\NumericField
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
  *
- * @uses \MacFJA\RediSearch\Index\Builder\AbstractField
- * @uses \MacFJA\RediSearch\Helper\RedisHelper
+ * @covers \MacFJA\RediSearch\Redis\Command\AliasAdd
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
  */
-class NumericFieldTest extends TestCase
+class AliasAddTest extends TestCase
 {
-    use Assertion;
-
-    public function testNominal(): void
+    public function testGetId(): void
     {
-        self::assertSameQuery('age NUMERIC SORTABLE', new NumericField('age', true));
-        self::assertSameQuery('weight NUMERIC NOINDEX', new NumericField('weight', false, true));
-        self::assertSameQuery('speed NUMERIC SORTABLE NOINDEX', new NumericField('speed', true, true));
+        $command = new AliasAdd();
+        static::assertSame('FT.ALIASADD', $command->getId());
     }
 
-    public function testGetType(): void
+    public function testFullOption(): void
     {
-        self::assertSame('NUMERIC', (new NumericField('foobar'))->getType());
+        $command = new AliasAdd();
+        $command
+            ->setIndex('idx')
+            ->setAlias('current')
+            ;
+
+        static::assertSame([
+            'current', 'idx',
+        ], $command->getArguments());
     }
 }
diff --git a/tests/support/Assertion.php b/tests/Redis/Command/AliasDelTest.php
similarity index 58%
rename from tests/support/Assertion.php
rename to tests/Redis/Command/AliasDelTest.php
index 5f9cc61..05dba5d 100644
--- a/tests/support/Assertion.php
+++ b/tests/Redis/Command/AliasDelTest.php
@@ -19,30 +19,38 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace Tests\MacFJA\RediSearch\support;
+namespace MacFJA\RediSearch\tests\Redis\Command;
 
-use function implode;
-use function is_subclass_of;
-use MacFJA\RediSearch\PartialQuery;
-use MacFJA\RediSearch\Search\QueryBuilder\PartialQuery as RenderablePartialQuery;
+use MacFJA\RediSearch\Redis\Command\AliasDel;
 use PHPUnit\Framework\TestCase;
-use RuntimeException;
 
-trait Assertion
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\AliasDel
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class AliasDelTest extends TestCase
 {
-    private static function assertSameQuery(string $expected, PartialQuery $partialQuery): void
+    public function testGetId(): void
     {
-        if (!is_subclass_of(self::class, TestCase::class)) {
-            throw new RuntimeException();
-        }
-        self::assertSame($expected, implode(' ', $partialQuery->getQueryParts()));
+        $command = new AliasDel();
+        static::assertSame('FT.ALIASDEL', $command->getId());
     }
 
-    private static function assertSameRender(string $expected, RenderablePartialQuery $partialQuery): void
+    public function testFullOption(): void
     {
-        if (!is_subclass_of(self::class, TestCase::class)) {
-            throw new RuntimeException();
-        }
-        self::assertSame($expected, $partialQuery->render());
+        $command = new AliasDel();
+        $command
+            ->setAlias('current')
+            ;
+
+        static::assertSame([
+            'current',
+        ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/AliasUpdateTest.php b/tests/Redis/Command/AliasUpdateTest.php
new file mode 100644
index 0000000..12c17f1
--- /dev/null
+++ b/tests/Redis/Command/AliasUpdateTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\AliasUpdate;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\AliasUpdate
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class AliasUpdateTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new AliasUpdate();
+        static::assertSame('FT.ALIASUPDATE', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new AliasUpdate();
+        $command
+            ->setIndex('idx')
+            ->setAlias('current')
+            ;
+
+        static::assertSame([
+            'current', 'idx',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/AlterTest.php b/tests/Redis/Command/AlterTest.php
new file mode 100644
index 0000000..02af61e
--- /dev/null
+++ b/tests/Redis/Command/AlterTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Alter;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\Alter
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ *
+ * @internal
+ */
+class AlterTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new Alter();
+        static::assertSame('FT.ALTER', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new Alter();
+        $command
+            ->setIndex('idx')
+            ->addNumericField('id2', true, false)
+            ;
+
+        static::assertSame([
+            'idx', 'SCHEMA', 'ADD', 'id2', 'NUMERIC', 'SORTABLE',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/ConfigSetTest.php b/tests/Redis/Command/ConfigSetTest.php
new file mode 100644
index 0000000..c5d4143
--- /dev/null
+++ b/tests/Redis/Command/ConfigSetTest.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\ConfigSet;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\ConfigSet
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ *
+ * @internal
+ */
+class ConfigSetTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new ConfigSet();
+        static::assertSame('FT.CONFIG', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new ConfigSet();
+        $command->setConfig('TIMEOUT', 3);
+        static::assertSame(['SET', 'TIMEOUT', 3], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/ConfigTest.php b/tests/Redis/Command/ConfigTest.php
new file mode 100644
index 0000000..89baf75
--- /dev/null
+++ b/tests/Redis/Command/ConfigTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Config;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\Config
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ *
+ * @internal
+ */
+class ConfigTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new Config();
+        static::assertSame('FT.CONFIG', $command->getId());
+    }
+
+    public function testGetAll(): void
+    {
+        $command = new Config();
+        $command->setGetMode();
+        static::assertSame(['GET', '*'], $command->getArguments());
+    }
+
+    public function testGetOne(): void
+    {
+        $command = new Config();
+        $command->setGetMode('foo');
+        static::assertSame(['GET', 'foo'], $command->getArguments());
+    }
+
+    public function testHelpAll(): void
+    {
+        $command = new Config();
+        $command->setHelpMode();
+        static::assertSame(['HELP', '*'], $command->getArguments());
+    }
+
+    public function testHelpOne(): void
+    {
+        $command = new Config();
+        $command->setHelpMode('foo');
+        static::assertSame(['HELP', 'foo'], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/CreateTest.php b/tests/Redis/Command/CreateTest.php
new file mode 100644
index 0000000..2df8579
--- /dev/null
+++ b/tests/Redis/Command/CreateTest.php
@@ -0,0 +1,166 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Create;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\Create
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ *
+ * @internal
+ */
+class CreateTest extends TestCase
+{
+    public function testGetArguments(): void
+    {
+        $command = new Create();
+        $command->setDefaultLanguage('french')
+            ->setIndex('idx')
+            ->setPrefixes('document_', 'doc_')
+            ->addTextField('title', false, null, null, true)
+            ->addTextField('subtitle', true, null, null, false, true)
+            ->addGeoField('city')
+            ->addNumericField('views', true)
+        ;
+
+        static::assertSame([
+            'idx',
+            'PREFIX', 2,  'document_', 'doc_',
+            'LANGUAGE', 'french',
+            'SCHEMA',
+            'title', 'TEXT', 'SORTABLE',
+            'subtitle', 'TEXT', 'NOSTEM', 'NOINDEX',
+            'city', 'GEO',
+            'views', 'NUMERIC', 'SORTABLE',
+        ], $command->getArguments());
+    }
+
+    public function testGetId(): void
+    {
+        $command = new Create();
+        static::assertSame('FT.CREATE', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new Create();
+        $command
+            ->addNumericField('num1')
+            ->addNumericField('num2', false, true)
+            ->addNumericField('num3', true, false)
+            ->addNumericField('num4', true, true)
+            ->addGeoField('geo1')
+            ->addGeoField('geo2', true)
+            ->addTextField('text1')
+            ->addTextField('text2', true)
+            ->addTextField('text3', false, 1.0, )
+            ->addTextField('text4', false, null, 'dm:en')
+            ->addTextField('text5', false, null, null, true)
+            ->addTextField('text6', false, null, null, false, true)
+            ->addTextField('text7', true, 1.0, 'dm:fr', true, true)
+            ->addTagField('tag1')
+            ->addTagField('tag2', '|')
+            ->addTagField('tag3', null, true)
+            ->addTagField('tag4', null, false, true)
+            ->addTagField('tag5', '#', true, true)
+            ->setPrefixes('doc_', 'document_')
+            ->setIndex('idx')
+            ->setDefaultLanguage('french')
+            ->setDefaultScore(1.5)
+            ->setFilter('startswith(@text1, "G")')
+            ->setLanguageField('text6')
+            ->setMaxTextFields(true)
+            ->setNoFields(true)
+            ->setNoFrequencies(true)
+            ->setNoHighLight(true)
+            ->setNoOffsets(true)
+            ->setPayloadField('text4')
+            ->setScoreField('num1')
+            ->setSkipInitialScan(true)
+            ->setStopWords('the', 'a')
+            ->setTemporary(3600)
+            ->setStructure('HASH')
+        ;
+
+        static::assertSame([
+            'idx',
+            'ON', 'HASH',
+            'PREFIX', 2, 'doc_', 'document_',
+            'FILTER', 'startswith(@text1, "G")',
+            'LANGUAGE', 'french',
+            'LANGUAGE_FIELD', 'text6',
+            'SCORE', 1.5,
+            'SCORE_FIELD', 'num1',
+            'PAYLOAD_FIELD', 'text4',
+            'MAXTEXTFIELDS', 'TEMPORARY', 3600, 'NOOFFSETS', 'NOHL', 'NOFIELDS', 'NOFREQS', 'SKIPINITIALSCAN',
+            'STOPWORDS', 2, 'the', 'a',
+            'SCHEMA',
+            'num1', 'NUMERIC',
+            'num2', 'NUMERIC', 'NOINDEX',
+            'num3', 'NUMERIC', 'SORTABLE',
+            'num4', 'NUMERIC', 'SORTABLE', 'NOINDEX',
+            'geo1', 'GEO',
+            'geo2', 'GEO', 'NOINDEX',
+            'text1', 'TEXT',
+            'text2', 'TEXT', 'NOSTEM',
+            'text3', 'TEXT', 'WEIGHT', 1.0,
+            'text4', 'TEXT', 'PHONETIC', 'dm:en',
+            'text5', 'TEXT', 'SORTABLE',
+            'text6', 'TEXT', 'NOINDEX',
+            'text7', 'TEXT', 'NOSTEM', 'WEIGHT', 1.0, 'PHONETIC', 'dm:fr', 'SORTABLE', 'NOINDEX',
+            'tag1', 'TAG',
+            'tag2', 'TAG', 'SEPARATOR', '|',
+            'tag3', 'TAG', 'SORTABLE',
+            'tag4', 'TAG', 'NOINDEX',
+            'tag5', 'TAG', 'SEPARATOR', '#', 'SORTABLE', 'NOINDEX',
+        ], $command->getArguments());
+    }
+
+    public function testNoStopWords(): void
+    {
+        $command = new Create();
+        $command->setIndex('idx')
+            ->addTextField('foo')
+            ->setNoStopWords()
+        ;
+
+        static::assertSame(['idx', 'STOPWORDS', 0, 'SCHEMA', 'foo', 'TEXT'], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/CursorDelTest.php b/tests/Redis/Command/CursorDelTest.php
new file mode 100644
index 0000000..bc9ab46
--- /dev/null
+++ b/tests/Redis/Command/CursorDelTest.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\CursorDel;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\CursorDel
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ *
+ * @internal
+ */
+class CursorDelTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new CursorDel();
+        static::assertSame('FT.CURSOR', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new CursorDel();
+        $command->setIndex('idx');
+        $command->setCursorId(398);
+        static::assertSame(['DEL', 'idx', 398], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/CursorReadTest.php b/tests/Redis/Command/CursorReadTest.php
new file mode 100644
index 0000000..da7c0fd
--- /dev/null
+++ b/tests/Redis/Command/CursorReadTest.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\CursorRead;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\CursorRead
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ *
+ * @internal
+ */
+class CursorReadTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new CursorRead();
+        static::assertSame('FT.CURSOR', $command->getId());
+    }
+
+    public function testCountOption(): void
+    {
+        $command = new CursorRead();
+        $command->setIndex('idx');
+        $command->setCursorId(398);
+        $command->setCount(30);
+        static::assertSame(['READ', 'idx', 398, 'COUNT', 30], $command->getArguments());
+
+        $command->setNoCount();
+        static::assertSame(['READ', 'idx', 398], $command->getArguments());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new CursorRead();
+        $command->setIndex('idx');
+        $command->setCursorId(398);
+        $command->setCount(30);
+        static::assertSame(['READ', 'idx', 398, 'COUNT', 30], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/DictAddTest.php b/tests/Redis/Command/DictAddTest.php
new file mode 100644
index 0000000..ad8d648
--- /dev/null
+++ b/tests/Redis/Command/DictAddTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\DictAdd;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\DictAdd
+ **
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class DictAddTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new DictAdd();
+        static::assertSame('FT.DICTADD', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new DictAdd();
+        $command
+            ->setDictionary('cities')
+            ->addTerms('paris', 'new york', 'london')
+            ;
+
+        static::assertSame([
+            'cities',
+            'paris', 'new york', 'london',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/DictDelTest.php b/tests/Redis/Command/DictDelTest.php
new file mode 100644
index 0000000..f4a1c1e
--- /dev/null
+++ b/tests/Redis/Command/DictDelTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\DictDel;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\DictDel
+ **
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class DictDelTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new DictDel();
+        static::assertSame('FT.DICTDEL', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new DictDel();
+        $command
+            ->setDictionary('cities')
+            ->addTerms('paris', 'new york', 'london')
+            ;
+
+        static::assertSame([
+            'cities',
+            'paris', 'new york', 'london',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/DictDumpTest.php b/tests/Redis/Command/DictDumpTest.php
new file mode 100644
index 0000000..c0e533f
--- /dev/null
+++ b/tests/Redis/Command/DictDumpTest.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\DictDump;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\DictDump
+ **
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class DictDumpTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new DictDump();
+        static::assertSame('FT.DICTDUMP', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new DictDump();
+        $command
+            ->setDictionary('cities')
+            ;
+
+        static::assertSame([
+            'cities',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/DropIndexTest.php b/tests/Redis/Command/DropIndexTest.php
new file mode 100644
index 0000000..384b969
--- /dev/null
+++ b/tests/Redis/Command/DropIndexTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\DropIndex;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\DropIndex
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ *
+ * @internal
+ */
+class DropIndexTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new DropIndex();
+        static::assertSame('FT.DROPINDEX', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new DropIndex();
+        $command
+            ->setIndex('idx')
+            ->setDeleteDocument(true)
+            ;
+
+        static::assertSame([
+            'idx', 'DD',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/ExplainCliTest.php b/tests/Redis/Command/ExplainCliTest.php
new file mode 100644
index 0000000..1569fab
--- /dev/null
+++ b/tests/Redis/Command/ExplainCliTest.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\ExplainCli;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Redis\Command\Explain
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\ExplainCli
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class ExplainCliTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new ExplainCli();
+        static::assertSame('FT.EXPLAINCLI', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new ExplainCli();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/ExplainTest.php b/tests/Redis/Command/ExplainTest.php
new file mode 100644
index 0000000..bc15225
--- /dev/null
+++ b/tests/Redis/Command/ExplainTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Explain;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\Explain
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class ExplainTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new Explain();
+        static::assertSame('FT.EXPLAIN', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new Explain();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/IndexListTest.php b/tests/Redis/Command/IndexListTest.php
new file mode 100644
index 0000000..97b5737
--- /dev/null
+++ b/tests/Redis/Command/IndexListTest.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\IndexList;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\IndexList
+ *
+ * @internal
+ */
+class IndexListTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new IndexList();
+        static::assertSame('FT._LIST', $command->getId());
+    }
+}
diff --git a/tests/Index/Builder/GeoFieldTest.php b/tests/Redis/Command/InfoTest.php
similarity index 61%
rename from tests/Index/Builder/GeoFieldTest.php
rename to tests/Redis/Command/InfoTest.php
index 1c54c41..e28ccac 100644
--- a/tests/Index/Builder/GeoFieldTest.php
+++ b/tests/Redis/Command/InfoTest.php
@@ -19,31 +19,38 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace Tests\MacFJA\RediSearch\Index\Builder;
+namespace MacFJA\RediSearch\tests\Redis\Command;
 
-use MacFJA\RediSearch\Index\Builder\GeoField;
+use MacFJA\RediSearch\Redis\Command\Info;
 use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
 
 /**
- * @covers \MacFJA\RediSearch\Index\Builder\GeoField
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
  *
- * @uses \MacFJA\RediSearch\Index\Builder\AbstractField
- * @uses \MacFJA\RediSearch\Helper\RedisHelper
+ * @covers \MacFJA\RediSearch\Redis\Command\Info
+ **
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
  */
-class GeoFieldTest extends TestCase
+class InfoTest extends TestCase
 {
-    use Assertion;
-
-    public function testNominal(): void
+    public function testGetId(): void
     {
-        self::assertSameQuery('address GEO NOINDEX', new GeoField('address', true));
-        self::assertSameQuery('position GEO', new GeoField('position', false));
-        self::assertSameQuery('coordinate GEO', new GeoField('coordinate'));
+        $command = new Info();
+        static::assertSame('FT.INFO', $command->getId());
     }
 
-    public function testGetType(): void
+    public function testFullOption(): void
     {
-        self::assertSame('GEO', (new GeoField('foobar'))->getType());
+        $command = new Info();
+        $command
+            ->setIndex('idx')
+            ;
+
+        static::assertSame([
+            'idx',
+        ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/Option/AbstractCommandOptionTest.php b/tests/Redis/Command/Option/AbstractCommandOptionTest.php
new file mode 100644
index 0000000..8f15150
--- /dev/null
+++ b/tests/Redis/Command/Option/AbstractCommandOptionTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\Option\FakeAbstractCommandOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class AbstractCommandOptionTest extends \PHPUnit\Framework\TestCase
+{
+    public function testIsCompatible(): void
+    {
+        $noConstraint = new FakeAbstractCommandOption();
+        static::assertTrue($noConstraint->isCompatible(null));
+        static::assertFalse($noConstraint->isCompatible('foo'));
+        static::assertTrue($noConstraint->isCompatible('1.0.0'));
+        static::assertTrue($noConstraint->isCompatible('1.5.0'));
+        static::assertTrue($noConstraint->isCompatible('2.0.0'));
+        static::assertTrue($noConstraint->isCompatible('2.5.0'));
+
+        $fixConstraint = new FakeAbstractCommandOption('1.5.0');
+        static::assertTrue($fixConstraint->isCompatible(null));
+        static::assertFalse($fixConstraint->isCompatible('foo'));
+        static::assertFalse($fixConstraint->isCompatible('1.0.0'));
+        static::assertTrue($fixConstraint->isCompatible('1.5.0'));
+        static::assertFalse($fixConstraint->isCompatible('2.0.0'));
+        static::assertFalse($fixConstraint->isCompatible('2.5.0'));
+
+        $simpleInterval = new FakeAbstractCommandOption('^1.1.0');
+        static::assertTrue($simpleInterval->isCompatible(null));
+        static::assertFalse($simpleInterval->isCompatible('foo'));
+        static::assertFalse($simpleInterval->isCompatible('1.0.0'));
+        static::assertTrue($simpleInterval->isCompatible('1.5.0'));
+        static::assertFalse($simpleInterval->isCompatible('2.0.0'));
+        static::assertFalse($simpleInterval->isCompatible('2.5.0'));
+
+        $simpleInterval2 = new FakeAbstractCommandOption('>=1.1.0');
+        static::assertTrue($simpleInterval2->isCompatible(null));
+        static::assertFalse($simpleInterval2->isCompatible('foo'));
+        static::assertFalse($simpleInterval2->isCompatible('1.0.0'));
+        static::assertTrue($simpleInterval2->isCompatible('1.5.0'));
+        static::assertTrue($simpleInterval2->isCompatible('2.0.0'));
+        static::assertTrue($simpleInterval2->isCompatible('2.5.0'));
+
+        $multiInterval = new FakeAbstractCommandOption('>=1.1.0 <=2.4');
+        static::assertTrue($multiInterval->isCompatible(null));
+        static::assertFalse($multiInterval->isCompatible('foo'));
+        static::assertFalse($multiInterval->isCompatible('1.0.0'));
+        static::assertTrue($multiInterval->isCompatible('1.5.0'));
+        static::assertTrue($multiInterval->isCompatible('2.0.0'));
+        static::assertFalse($multiInterval->isCompatible('2.5.0'));
+
+        /** @var FakeAbstractCommandOption $invalidConstraint */
+        $invalidConstraint = new FakeAbstractCommandOption('bar');
+        static::assertTrue($invalidConstraint->isCompatible(null));
+        static::assertFalse($invalidConstraint->isCompatible('foo'));
+        static::assertFalse($invalidConstraint->isCompatible('1.0.0'));
+        static::assertFalse($invalidConstraint->isCompatible('1.5.0'));
+        static::assertFalse($invalidConstraint->isCompatible('2.0.0'));
+        static::assertFalse($invalidConstraint->isCompatible('2.5.0'));
+    }
+
+    public function testGetVersionConstraint(): void
+    {
+        $noConstraint = new FakeAbstractCommandOption();
+        static::assertSame('*', $noConstraint->getVersionConstraint());
+
+        $fixConstraint = new FakeAbstractCommandOption('1.5.0');
+        static::assertSame('1.5.0', $fixConstraint->getVersionConstraint());
+
+        $simpleInterval = new FakeAbstractCommandOption('^1.1.0');
+        static::assertSame('^1.1.0', $simpleInterval->getVersionConstraint());
+    }
+}
diff --git a/src/Configuration.php b/tests/Redis/Command/Option/BaseOptionTestTrait.php
similarity index 55%
rename from src/Configuration.php
rename to tests/Redis/Command/Option/BaseOptionTestTrait.php
index 84904df..fe102a6 100644
--- a/src/Configuration.php
+++ b/tests/Redis/Command/Option/BaseOptionTestTrait.php
@@ -19,47 +19,43 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch;
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
 
-use function is_scalar;
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use Predis\Client;
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
 
-class Configuration
+trait BaseOptionTestTrait
 {
-    /** @var Client */
-    private $redis;
-
-    public function __construct(Client $redis)
+    /**
+     * @dataProvider dataProvider
+     *
+     * @param array<float|int|string> $expected
+     */
+    public function testRender(CommandOption $inputOption, array $expected): void
     {
-        $this->redis = $redis;
+        self::assertEquals($expected, $inputOption->render());
     }
 
-    public function getOption(string $optionName): string
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testIsValid(CommandOption $inputOption, bool $expected): void
     {
-        return $this->redis->executeRaw(['FT.CONFIG', 'GET', $optionName]);
+        self::assertEquals($expected, $inputOption->isValid());
     }
 
     /**
-     * @return array<float|int|string>
+     * @dataProvider dataProvider
+     *
+     * @param mixed $expected
      */
-    public function getAll(): array
+    public function testGetOptionData(CommandOption $inputOption, $expected): void
     {
-        $rawResult = $this->redis->executeRaw(['FT.CONFIG', 'GET', '*']);
-
-        return RedisHelper::getPairs($rawResult);
+        self::assertEquals($expected, $inputOption->getOptionData());
     }
 
     /**
-     * @param float|int|string $value
-     * @psalm-suppress RedundantConditionGivenDocblockType
+     * @return Generator<array>
      */
-    public function setOption(string $optionName, $value): bool
-    {
-        DataHelper::assert(is_scalar($value));
-        $rawResult = $this->redis->executeRaw(['FT.CONFIG', 'GET', $optionName, $value]);
-
-        return 'OK' === $rawResult;
-    }
+    abstract public function dataProvider(string $testName): Generator;
 }
diff --git a/tests/Redis/Command/Option/CustomValidatorOptionTest.php b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
new file mode 100644
index 0000000..5034a4f
--- /dev/null
+++ b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
@@ -0,0 +1,122 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Respect\Validation\Rules\Length;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class CustomValidatorOptionTest extends \PHPUnit\Framework\TestCase
+{
+    use BaseOptionTestTrait;
+    use TestIsCompatibleTrait;
+
+    public function testGetValidator(): void
+    {
+        $validator = new Length(4);
+        $option = new CustomValidatorOption(new NamelessOption(), $validator);
+
+        static::assertSame($validator, $option->getValidator());
+    }
+
+    public function testShorthands(): void
+    {
+        $validNumeric = CustomValidatorOption::isNumeric(new NamelessOption('10'));
+        $invalidNumeric = CustomValidatorOption::isNumeric(new NamelessOption('foo'));
+        $validList = CustomValidatorOption::allowedValues(new NamelessOption('foo'), ['foo', 'bar']);
+        $invalidList = CustomValidatorOption::allowedValues(new NamelessOption('foobar'), ['foo', 'bar']);
+
+        static::assertTrue($validNumeric->isValid());
+        static::assertTrue($validList->isValid());
+        static::assertFalse($invalidNumeric->isValid());
+        static::assertFalse($invalidList->isValid());
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $invalidMinLengthOption = new CustomValidatorOption(new NamelessOption('foo'), new Length(4));
+        $emptyMinLengthOption = new CustomValidatorOption(new NamelessOption(), new Length(4));
+        $validMinLengthOption = new CustomValidatorOption(new NamelessOption('foobar'), new Length(4));
+        $lowValidMinLengthOption = new CustomValidatorOption(new NamelessOption('foobar', '>=1.0.0'), new Length(4));
+        $highValidMinLengthOption = new CustomValidatorOption(new NamelessOption('foobar', '>=2.0.0'), new Length(4));
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$invalidMinLengthOption, 'foo'];
+                yield [$validMinLengthOption, 'foobar'];
+                yield [$emptyMinLengthOption, null];
+
+                break;
+
+            case 'testIsValid':
+                yield [$invalidMinLengthOption, false];
+                yield [$validMinLengthOption, true];
+                yield [$emptyMinLengthOption, false];
+
+                break;
+
+            case 'testRender':
+                yield [$invalidMinLengthOption, []];
+                yield [$validMinLengthOption, ['foobar']];
+                yield [$emptyMinLengthOption, []];
+
+                break;
+
+            case 'testIsCompatible':
+                yield [$validMinLengthOption, '1.0.0', true];
+                yield [$validMinLengthOption, '1.5.0', true];
+                yield [$validMinLengthOption, '2.0.0', true];
+                yield [$validMinLengthOption, '2.5.0', true];
+                yield [$validMinLengthOption, 'foo', false];
+
+                yield [$lowValidMinLengthOption, '1.0.0', true];
+                yield [$lowValidMinLengthOption, '1.5.0', true];
+                yield [$lowValidMinLengthOption, '2.0.0', true];
+                yield [$lowValidMinLengthOption, '2.5.0', true];
+                yield [$lowValidMinLengthOption, 'foo', false];
+
+                yield [$highValidMinLengthOption, '1.0.0', false];
+                yield [$highValidMinLengthOption, '1.5.0', false];
+                yield [$highValidMinLengthOption, '2.0.0', true];
+                yield [$highValidMinLengthOption, '2.5.0', true];
+                yield [$highValidMinLengthOption, 'foo', false];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/FlagOptionTest.php b/tests/Redis/Command/Option/FlagOptionTest.php
new file mode 100644
index 0000000..b3b6c36
--- /dev/null
+++ b/tests/Redis/Command/Option/FlagOptionTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class FlagOptionTest extends TestCase
+{
+    use BaseOptionTestTrait;
+
+    public function testGetSetActive(): void
+    {
+        $option = new FlagOption('FOO', true);
+        static::assertTrue($option->isActive());
+        $option->setActive(true);
+        static::assertTrue($option->isActive());
+        $option->setActive(false);
+        static::assertFalse($option->isActive());
+        $option = new FlagOption('BAR', false);
+        static::assertFalse($option->isActive());
+        $option = new FlagOption('FOOBAR');
+        static::assertFalse($option->isActive());
+    }
+
+    public function testGetName(): void
+    {
+        $option = new FlagOption('FOO');
+        static::assertEquals('FOO', $option->getName());
+        $option = new FlagOption('BAR');
+        static::assertEquals('BAR', $option->getName());
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $defaultValueFlag = new FlagOption('FOO');
+        $notActiveFlag = new FlagOption('FOO', false);
+        $activeFlag = new FlagOption('BAR', true);
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$defaultValueFlag, false];
+                yield [$notActiveFlag, false];
+                yield [$activeFlag, true];
+
+                break;
+
+            case 'testIsValid':
+                yield [$defaultValueFlag, true];
+                yield [$notActiveFlag, true];
+                yield [$activeFlag, true];
+
+                break;
+
+            case 'testRender':
+                yield [$defaultValueFlag, []];
+                yield [$notActiveFlag, []];
+                yield [$activeFlag, ['BAR']];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/GroupedOptionTest.php b/tests/Redis/Command/Option/GroupedOptionTest.php
new file mode 100644
index 0000000..b45ef71
--- /dev/null
+++ b/tests/Redis/Command/Option/GroupedOptionTest.php
@@ -0,0 +1,196 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use BadMethodCallException;
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+use OutOfRangeException;
+use PHPUnit\Framework\TestCase;
+use Respect\Validation\Rules\StringVal;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ *
+ * @internal
+ */
+class GroupedOptionTest extends TestCase
+{
+    use BaseOptionTestTrait;
+    use TestIsCompatibleTrait;
+
+    public function testGetSetDataOfOption(): void
+    {
+        $option = new GroupedOption([
+            'flag' => new FlagOption('FOO'),
+            'nameless' => new NamelessOption(),
+            'decorated' => new CustomValidatorOption(new NamelessOption(), new StringVal()),
+            'named' => new NamedOption('BAR'),
+            'list' => new NumberedOption('list'),
+        ], []);
+
+        static::assertEquals(false, $option->getDataOfOption('flag'));
+        static::assertEquals(null, $option->getDataOfOption('nameless'));
+        static::assertEquals(null, $option->getDataOfOption('decorated'));
+        static::assertEquals(null, $option->getDataOfOption('named'));
+        static::assertEquals(null, $option->getDataOfOption('list'));
+
+        $option->setDataOfOption('flag', true);
+        static::assertEquals(true, $option->getDataOfOption('flag'));
+
+        $option->setDataOfOption('nameless', 20);
+        static::assertEquals(20, $option->getDataOfOption('nameless'));
+
+        $option->setDataOfOption('decorated', 'foo');
+        static::assertEquals('foo', $option->getDataOfOption('decorated'));
+
+        $option->setDataOfOption('list', ['foo', 'bar']);
+        static::assertEquals(['foo', 'bar'], $option->getDataOfOption('list'));
+    }
+
+    public function testGetOption(): void
+    {
+        $option = new GroupedOption([
+            'flag' => new FlagOption('FOO'),
+            'nameless' => new NamelessOption(),
+            'named' => new NamedOption('BAR'),
+        ], []);
+
+        static::assertEquals(new FlagOption('FOO'), $option->getOption('flag'));
+        static::assertEquals(new NamelessOption(), $option->getOption('nameless'));
+        static::assertEquals(new NamedOption('BAR'), $option->getOption('named'));
+    }
+
+    public function testGetOptionsName(): void
+    {
+        $option = new GroupedOption([
+            'flag' => new FlagOption('FOO'),
+            'nameless' => new NamelessOption(),
+            'named' => new NamedOption('BAR'),
+        ], []);
+        static::assertEquals(['flag', 'nameless', 'named'], $option->getOptionsName());
+    }
+
+    public function testSetUnknownOptionData(): void
+    {
+        $this->expectException(OutOfRangeException::class);
+        $this->expectErrorMessage('There is no option for foobar');
+
+        $option = new GroupedOption([], []);
+        $option->setDataOfOption('foobar', 'bar');
+    }
+
+    public function testSetLockOptionData(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectErrorMessage('The option lock can\'t be modified');
+
+        $option = new GroupedOption(['lock' => new NamelessOption('foo')], [], ['lock']);
+        $option->setDataOfOption('lock', 'bar');
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $emptyGroup = new GroupedOption([], []);
+        $noRequiredGroup = new GroupedOption(['foo' => new NamelessOption()], []);
+        $emptyRequiredGroup = new GroupedOption([], ['foo']);
+        $noValueRequireGroup = new GroupedOption(['foo' => new NamelessOption()], ['foo']);
+        $valueRequireGroup = new GroupedOption(['foo' => new NamelessOption('FOO')], ['foo']);
+        $valueRequireGroup2 = new GroupedOption(['foo' => new NamelessOption(0)], ['foo']);
+        $valueLockGroup = new GroupedOption(['foo' => new NamelessOption('FOO')], [], ['foo']);
+        $lowVersionGroup = new GroupedOption([], [], [], '>=1.0.0');
+        $highVersionGroup = new GroupedOption([], [], [], '>=2.0.0');
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$emptyGroup, []];
+                yield [$noRequiredGroup, ['foo' => new NamelessOption()]];
+                yield [$emptyRequiredGroup, []];
+                yield [$noValueRequireGroup, ['foo' => new NamelessOption()]];
+                yield [$valueRequireGroup, ['foo' => new NamelessOption('FOO')]];
+                yield [$valueRequireGroup2, ['foo' => new NamelessOption(0)]];
+                yield [$valueLockGroup, ['foo' => new NamelessOption('FOO')]];
+
+                break;
+
+            case 'testIsValid':
+                yield [$emptyGroup, true];
+                yield [$noRequiredGroup, true];
+                yield [$emptyRequiredGroup, false];
+                yield [$noValueRequireGroup, false];
+                yield [$valueRequireGroup, true];
+                yield [$valueRequireGroup2, true];
+                yield [$valueLockGroup, true];
+
+                break;
+
+            case 'testRender':
+                yield [$emptyGroup, []];
+                yield [$noRequiredGroup, []];
+                yield [$emptyRequiredGroup, []];
+                yield [$noValueRequireGroup, []];
+                yield [$valueRequireGroup, ['FOO']];
+                yield [$valueRequireGroup2, [0]];
+                yield [$valueLockGroup, ['FOO']];
+
+                break;
+
+            case 'testIsCompatible':
+                yield [$emptyGroup, '1.0.0', true];
+                yield [$emptyGroup, '1.5.0', true];
+                yield [$emptyGroup, '2.0.0', true];
+                yield [$emptyGroup, '2.5.0', true];
+
+                yield [$lowVersionGroup, '1.0.0', true];
+                yield [$lowVersionGroup, '1.5.0', true];
+                yield [$lowVersionGroup, '2.0.0', true];
+                yield [$lowVersionGroup, '2.5.0', true];
+
+                yield [$highVersionGroup, '1.0.0', false];
+                yield [$highVersionGroup, '1.5.0', false];
+                yield [$highVersionGroup, '2.0.0', true];
+                yield [$highVersionGroup, '2.5.0', true];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/NamedOptionTest.php b/tests/Redis/Command/Option/NamedOptionTest.php
new file mode 100644
index 0000000..306dad8
--- /dev/null
+++ b/tests/Redis/Command/Option/NamedOptionTest.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class NamedOptionTest extends TestCase
+{
+    use BaseOptionTestTrait;
+
+    public function testGetSetValue(): void
+    {
+        $option = new NamedOption('FOO');
+        static::assertEquals(null, $option->getValue());
+        $option->setValue('FOO');
+        static::assertEquals('FOO', $option->getValue());
+        $option->setValue('bar');
+        static::assertEquals('bar', $option->getValue());
+        $option->setValue(10);
+        static::assertEquals(10, $option->getValue());
+    }
+
+    public function testGetName(): void
+    {
+        $option = new NamedOption('FOO');
+        static::assertEquals('FOO', $option->getName());
+        $option = new NamedOption('BAR');
+        static::assertEquals('BAR', $option->getName());
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $defaultValueOption = new NamedOption('FOO');
+        $withValueOption = new NamedOption('FOO', 'bar');
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$defaultValueOption, null];
+                yield [$withValueOption, 'bar'];
+
+                break;
+
+            case 'testIsValid':
+                yield [$defaultValueOption, false];
+                yield [$withValueOption, true];
+
+                break;
+
+            case 'testRender':
+                yield [$defaultValueOption, []];
+                yield [$withValueOption, ['FOO', 'bar']];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/NamelessOptionTest.php b/tests/Redis/Command/Option/NamelessOptionTest.php
new file mode 100644
index 0000000..ab37d32
--- /dev/null
+++ b/tests/Redis/Command/Option/NamelessOptionTest.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class NamelessOptionTest extends TestCase
+{
+    use BaseOptionTestTrait;
+
+    public function testGetSetValue(): void
+    {
+        $option = new NamelessOption('FOO');
+        static::assertEquals('FOO', $option->getValue());
+        $option->setValue('FOO');
+        static::assertEquals('FOO', $option->getValue());
+        $option->setValue('bar');
+        static::assertEquals('bar', $option->getValue());
+        $option->setValue(10);
+        static::assertEquals(10, $option->getValue());
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $defaultValueOption = new NamelessOption();
+        $withValueOption = new NamelessOption('FOO');
+        $emptyValueOption = new NamelessOption('');
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$defaultValueOption, null];
+                yield [$withValueOption, 'FOO'];
+                yield [$emptyValueOption, ''];
+
+                break;
+
+            case 'testIsValid':
+                yield [$defaultValueOption, false];
+                yield [$withValueOption, true];
+                yield [$emptyValueOption, true];
+
+                break;
+
+            case 'testRender':
+                yield [$defaultValueOption, []];
+                yield [$withValueOption, ['FOO']];
+                yield [$emptyValueOption, ['']];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/NotEmptyOptionTest.php b/tests/Redis/Command/Option/NotEmptyOptionTest.php
new file mode 100644
index 0000000..b4a6ceb
--- /dev/null
+++ b/tests/Redis/Command/Option/NotEmptyOptionTest.php
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ *
+ * @internal
+ */
+class NotEmptyOptionTest extends \PHPUnit\Framework\TestCase
+{
+    use BaseOptionTestTrait;
+    use TestIsCompatibleTrait;
+
+    public function testIsValid2(): void
+    {
+        $option = new NotEmptyOption(new NamedOption('FOO'));
+        static::assertEquals(null, $option->getValue());
+        static::assertFalse($option->isValid());
+        static::assertSame([], $option->render());
+        $option->setValue('bar');
+        static::assertEquals('bar', $option->getValue());
+        static::assertTrue($option->isValid());
+        static::assertSame(['FOO', 'bar'], $option->render());
+
+        $option->setValue('');
+        static::assertEquals('', $option->getValue());
+        static::assertFalse($option->isValid());
+        static::assertSame([], $option->render());
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $namelessOption = new NotEmptyOption(new NamelessOption('foo'));
+        $namedOption = new NotEmptyOption(new NamedOption('FOO', 'bar'));
+        $numberedOption = new NotEmptyOption(new NumberedOption('FOO', ['bar', 'foobar']));
+        $emptyNamelessOption = new NotEmptyOption(new NamelessOption());
+        $emptyNamedOption = new NotEmptyOption(new NamedOption('FOO'));
+        $emptyNumberedOption = new NotEmptyOption(new NumberedOption('FOO', []));
+        $nullNumberedOption = new NotEmptyOption(new NumberedOption('FOO'));
+        $trueFlagOption = new NotEmptyOption(new FlagOption('FOO', true));
+        $falseFlagOption = new NotEmptyOption(new FlagOption('FOO'));
+        $lowOptionOption = new NotEmptyOption(new NamelessOption('bar', '>=1.0.0'));
+        $highOptionOption = new NotEmptyOption(new NamelessOption('bar', '>=2.0.0'));
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$namelessOption, 'foo'];
+                yield [$namedOption, 'bar'];
+                yield [$numberedOption, ['bar', 'foobar']];
+                yield [$emptyNamelessOption, null];
+                yield [$emptyNamedOption, null];
+                yield [$emptyNumberedOption, []];
+                yield [$nullNumberedOption, null];
+                yield [$trueFlagOption, true];
+                yield [$falseFlagOption, false];
+
+                break;
+
+            case 'testIsValid':
+                yield [$namelessOption, true];
+                yield [$namedOption, true];
+                yield [$numberedOption, true];
+                yield [$emptyNamelessOption, false];
+                yield [$emptyNamedOption, false];
+                yield [$emptyNumberedOption, false];
+                yield [$trueFlagOption, true];
+                yield [$falseFlagOption, false];
+
+                break;
+
+            case 'testRender':
+                yield [$namelessOption, ['foo']];
+                yield [$namedOption, ['FOO', 'bar']];
+                yield [$numberedOption, ['FOO', 2, 'bar', 'foobar']];
+                yield [$emptyNamelessOption, []];
+                yield [$emptyNamedOption, []];
+                yield [$emptyNumberedOption, []];
+                yield [$trueFlagOption, ['FOO']];
+                yield [$falseFlagOption, []];
+
+                break;
+
+            case 'testIsCompatible':
+                yield [$namelessOption, '1.0.0', true];
+                yield [$namelessOption, '1.5.0', true];
+                yield [$namelessOption, '2.0.0', true];
+                yield [$namelessOption, '2.5.0', true];
+                yield [$namelessOption, 'foo', false];
+
+                yield [$lowOptionOption, '1.0.0', true];
+                yield [$lowOptionOption, '1.5.0', true];
+                yield [$lowOptionOption, '2.0.0', true];
+                yield [$lowOptionOption, '2.5.0', true];
+                yield [$lowOptionOption, 'foo', false];
+
+                yield [$highOptionOption, '1.0.0', false];
+                yield [$highOptionOption, '1.5.0', false];
+                yield [$highOptionOption, '2.0.0', true];
+                yield [$highOptionOption, '2.5.0', true];
+                yield [$highOptionOption, 'foo', false];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/NumberedOptionTest.php b/tests/Redis/Command/Option/NumberedOptionTest.php
new file mode 100644
index 0000000..30173c7
--- /dev/null
+++ b/tests/Redis/Command/Option/NumberedOptionTest.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class NumberedOptionTest extends TestCase
+{
+    use BaseOptionTestTrait;
+
+    public function testGetSetArguments(): void
+    {
+        $option = new NumberedOption('FOO');
+        static::assertNull($option->getArguments());
+        $option->setArguments([]);
+        static::assertIsArray($option->getArguments());
+        static::assertEquals([], $option->getArguments());
+        $option->setArguments(['foo', 'bar', 'foobar']);
+        static::assertIsArray($option->getArguments());
+        static::assertEquals(['foo', 'bar', 'foobar'], $option->getArguments());
+
+        $option = new NumberedOption('FOO', ['foo']);
+        static::assertIsArray($option->getArguments());
+        static::assertEquals(['foo'], $option->getArguments());
+    }
+
+    public function testGetName(): void
+    {
+        $option = new NumberedOption('FOO');
+        static::assertEquals('FOO', $option->getName());
+        $option = new NumberedOption('BAR');
+        static::assertEquals('BAR', $option->getName());
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        $noArgumentsOption = new NumberedOption('FOO');
+        $emptyArgumentsOption = new NumberedOption('FOO', []);
+        $withOneArgumentOption = new NumberedOption('FOO', ['bar']);
+        $withMultipleArgumentOption = new NumberedOption('FOO', ['bar', 'foobar']);
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$noArgumentsOption, null];
+                yield [$emptyArgumentsOption, []];
+                yield [$withOneArgumentOption, ['bar']];
+                yield [$withMultipleArgumentOption, ['bar', 'foobar']];
+
+                break;
+
+            case 'testIsValid':
+                yield [$noArgumentsOption, false];
+                yield [$emptyArgumentsOption, true];
+                yield [$withOneArgumentOption, true];
+                yield [$withMultipleArgumentOption, true];
+
+                break;
+
+            case 'testRender':
+                yield [$noArgumentsOption, []];
+                yield [$emptyArgumentsOption, ['FOO', 0]];
+                yield [$withOneArgumentOption, ['FOO', 1, 'bar']];
+                yield [$withMultipleArgumentOption, ['FOO', 2, 'bar', 'foobar']];
+
+                break;
+
+            default:
+                return;
+        }
+    }
+}
diff --git a/tests/Redis/Command/Option/TestIsCompatibleTrait.php b/tests/Redis/Command/Option/TestIsCompatibleTrait.php
new file mode 100644
index 0000000..8baf947
--- /dev/null
+++ b/tests/Redis/Command/Option/TestIsCompatibleTrait.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
+
+trait TestIsCompatibleTrait
+{
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testIsCompatible(CommandOption $inputOption, string $inputVersion, bool $expected): void
+    {
+        $computed = $inputOption->isCompatible($inputVersion);
+        self::assertEquals($expected, $computed);
+    }
+}
diff --git a/tests/Redis/Command/SearchCommand/FilterOptionTest.php b/tests/Redis/Command/SearchCommand/FilterOptionTest.php
new file mode 100644
index 0000000..1fcf14b
--- /dev/null
+++ b/tests/Redis/Command/SearchCommand/FilterOptionTest.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\SearchCommand;
+
+use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class FilterOptionTest extends \PHPUnit\Framework\TestCase
+{
+    public function testShorthands(): void
+    {
+        static::assertSame(['FILTER', 'num1', '(100', '+inf'], FilterOption::greaterThan('num1', 100)->render());
+        static::assertSame(['FILTER', 'num1', '100', '100'], FilterOption::equalsTo('num1', 100)->render());
+        static::assertSame(['FILTER', 'num1', '100', '+inf'], FilterOption::greaterThanOrEquals('num1', 100)->render());
+        static::assertSame(['FILTER', 'num1', '-inf', '(100'], FilterOption::lessThan('num1', 100)->render());
+        static::assertSame(['FILTER', 'num1', '-inf', '100'], FilterOption::lessThanOrEquals('num1', 100)->render());
+    }
+
+    public function testGetOptionData(): void
+    {
+        static::assertSame([
+            'min' => 100.0,
+            'max' => null,
+            'isMinInclusive' => false,
+            'isMaxInclusive' => true,
+        ], FilterOption::greaterThan('num1', 100)->getOptionData());
+    }
+}
diff --git a/tests/Redis/Command/SearchCommand/LimitOptionTest.php b/tests/Redis/Command/SearchCommand/LimitOptionTest.php
new file mode 100644
index 0000000..c83d712
--- /dev/null
+++ b/tests/Redis/Command/SearchCommand/LimitOptionTest.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\SearchCommand;
+
+use MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ *
+ * @internal
+ */
+class LimitOptionTest extends \PHPUnit\Framework\TestCase
+{
+    public function testSetOffset(): void
+    {
+        $option = new LimitOption();
+        $option->setSize(1);
+
+        $option->setOffset(0);
+        static::assertSame(['LIMIT', 0, 1], $option->render());
+
+        $option->setOffset(10);
+        static::assertSame(['LIMIT', 10, 1], $option->render());
+
+        $option->setOffset(null);
+        static::assertSame([], $option->render());
+    }
+
+    public function testSetSize(): void
+    {
+        $option = new LimitOption();
+        $option->setOffset(0);
+
+        $option->setSize(0);
+        static::assertSame(['LIMIT', 0, 0], $option->render());
+
+        $option->setSize(10);
+        static::assertSame(['LIMIT', 0, 10], $option->render());
+
+        $option->setSize(null);
+        static::assertSame([], $option->render());
+    }
+
+    public function testIsValid(): void
+    {
+        $option = new LimitOption();
+        static::assertFalse($option->isValid());
+
+        $option->setOffset(0);
+        static::assertFalse($option->isValid());
+
+        $option->setSize(0);
+        static::assertTrue($option->isValid());
+
+        $option->setSize(-1);
+        static::assertFalse($option->isValid());
+
+        $option->setSize(10);
+        $option->setOffset(-10);
+        static::assertFalse($option->isValid());
+    }
+
+    public function testGetOptionData(): void
+    {
+        $option = new LimitOption();
+        static::assertSame(['offset' => null, 'size' => null], $option->getOptionData());
+
+        $option = new LimitOption();
+        $option->setOffset(0);
+        static::assertSame(['offset' => 0, 'size' => null], $option->getOptionData());
+
+        $option = new LimitOption();
+        $option->setSize(0);
+        static::assertSame(['offset' => null, 'size' => 0], $option->getOptionData());
+
+        $option = new LimitOption();
+        $option->setOffset(10);
+        $option->setSize(10);
+        static::assertSame(['offset' => 10, 'size' => 10], $option->getOptionData());
+    }
+}
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
new file mode 100644
index 0000000..824dd72
--- /dev/null
+++ b/tests/Redis/Command/SearchTest.php
@@ -0,0 +1,128 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Search;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\Search
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ *
+ * @internal
+ */
+class SearchTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new Search();
+        static::assertSame('FT.SEARCH', $command->getId());
+    }
+
+    public function testSizeOptions(): void
+    {
+        $command = new Search();
+        static::assertNull($command->getSize());
+        static::assertNull($command->getOffset());
+
+        $command->setLimit(0, 10);
+        static::assertSame(0, $command->getOffset());
+        static::assertSame(10, $command->getSize());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new Search();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setNoContent()
+            ->setNoStopWords()
+            ->setVerbatim()
+            ->setWithSortKeys()
+            ->setWithPayloads()
+            ->setWithScores()
+            ->addFilter(new FilterOption('@num1', 10, 20))
+            ->setGeoFilter('@geo1', 37.37767452939122, -122.06451753037919, 50, 'm')
+            ->setInKeys('@text1', '@text2')
+            ->setInFields('@text4')
+            ->setReturn('@text3', '@text4', '@text5')
+            ->setSummarize(['@text1', '@text3'], 2, 4, ' ... ')
+            ->setHighlight(['@text4'], '<em>', '</em>')
+            ->setSlop(2)
+            ->setInOrder()
+            ->setLanguage('english')
+            ->setExpander('my_expander')
+            ->setScorer('my_scorer')
+            ->setExplainScore()
+            ->setPayload('foobar')
+            ->setSortBy('@num1', 'ASC')
+            ->setLimit(12, 10)
+            ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'NOCONTENT',
+            'VERBATIM',
+            'NOSTOPWORDS',
+            'WITHSCORES',
+            'WITHPAYLOADS',
+            'WITHSORTKEYS',
+            'FILTER', '@num1', '10', '20',
+            'GEOFILTER', '@geo1', 37.37767452939122, -122.06451753037919, 50.0, 'm',
+            'INKEYS', 2, '@text1', '@text2',
+            'INFIELDS', 1, '@text4',
+            'RETURN', 3, '@text3', '@text4', '@text5',
+            'SUMMARIZE', 'FIELDS', 2, '@text1', '@text3', 'FRAGS', 2, 'LEN', 4, 'SEPARATOR', ' ... ',
+            'HIGHLIGHT', 'FIELDS', 1, '@text4', 'TAGS', '<em>', '</em>',
+            'SLOP', 2, 'INORDER',
+            'LANGUAGE', 'english',
+            'EXPANDER', 'my_expander',
+            'SCORER', 'my_scorer',
+            'EXPLAINSCORE',
+            'PAYLOAD', 'foobar',
+            'SORTBY', '@num1', 'ASC',
+            'LIMIT', 12, 10,
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php b/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php
new file mode 100644
index 0000000..84871b1
--- /dev/null
+++ b/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\SpellCheckCommand;
+
+use MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class TermsOptionTest extends \PHPUnit\Framework\TestCase
+{
+    public function testFullOptions(): void
+    {
+        $option = new TermsOption();
+        $option->setDictionary('country');
+        $option->setIsExclusion(true);
+        static::assertSame(['TERMS', 'EXCLUDE', 'country'], $option->render());
+
+        $option = new TermsOption();
+        $option->setDictionary('city');
+        $option->setIsExclusion(false);
+        static::assertSame(['TERMS', 'INCLUDE', 'city'], $option->render());
+    }
+
+    public function testGetData(): void
+    {
+        $option = new TermsOption();
+        $option->setDictionary('country');
+        $option->setIsExclusion(true);
+        static::assertSame('country', $option->getOptionData());
+
+        $option = new TermsOption();
+        $option->setDictionary('city');
+        $option->setIsExclusion(false);
+        static::assertSame('city', $option->getOptionData());
+    }
+}
diff --git a/tests/Redis/Command/SpellCheckTest.php b/tests/Redis/Command/SpellCheckTest.php
new file mode 100644
index 0000000..f83b9ca
--- /dev/null
+++ b/tests/Redis/Command/SpellCheckTest.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\SpellCheck;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SpellCheck
+ * @covers \MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption
+ **
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ *
+ * @internal
+ */
+class SpellCheckTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new SpellCheck();
+        static::assertSame('FT.SPELLCHECK', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new SpellCheck();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setDistance(2)
+            ->addTerms('badword', true)
+            ->addTerms('cities', false)
+            ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'DISTANCE', 2,
+            'TERMS', 'EXCLUDE', 'badword',
+            'TERMS', 'INCLUDE', 'cities',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/SugAddTest.php b/tests/Redis/Command/SugAddTest.php
new file mode 100644
index 0000000..cf6eae9
--- /dev/null
+++ b/tests/Redis/Command/SugAddTest.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\SugAdd;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SugAdd
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ *
+ * @internal
+ */
+class SugAddTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new SugAdd();
+        static::assertSame('FT.SUGADD', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new SugAdd();
+        $command
+            ->setDictionary('ac')
+            ->setSuggestion('hello world')
+            ->setScore(1)
+            ->setIncrement()
+            ->setPayload('greeting')
+            ;
+
+        static::assertSame([
+            'ac', 'hello world', 1.0, 'INCR', 'PAYLOAD', 'greeting',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/SugDelTest.php b/tests/Redis/Command/SugDelTest.php
new file mode 100644
index 0000000..17e2cd8
--- /dev/null
+++ b/tests/Redis/Command/SugDelTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\SugDel;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SugDel
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class SugDelTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new SugDel();
+        static::assertSame('FT.SUGDEL', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new SugDel();
+        $command
+            ->setDictionary('ac')
+            ->setSuggestion('hello world')
+            ;
+
+        static::assertSame([
+            'ac', 'hello world',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/SugGetTest.php b/tests/Redis/Command/SugGetTest.php
new file mode 100644
index 0000000..84c64b8
--- /dev/null
+++ b/tests/Redis/Command/SugGetTest.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\SugGet;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SugGet
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ *
+ * @internal
+ */
+class SugGetTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new SugGet();
+        static::assertSame('FT.SUGGET', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new SugGet();
+        $command
+            ->setDictionary('ac')
+            ->setPrefix('hell')
+            ->setFuzzy()
+            ->setWithScores()
+            ->setWithPayloads()
+            ->setMax(10)
+            ;
+
+        static::assertSame([
+            'ac', 'hell', 'FUZZY', 'WITHSCORES', 'WITHPAYLOADS', 'MAX', 10,
+        ], $command->getArguments());
+    }
+}
diff --git a/src/Search/Filter.php b/tests/Redis/Command/SugLenTest.php
similarity index 59%
rename from src/Search/Filter.php
rename to tests/Redis/Command/SugLenTest.php
index 76cf707..6ff5c72 100644
--- a/src/Search/Filter.php
+++ b/tests/Redis/Command/SugLenTest.php
@@ -19,33 +19,38 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Search;
+namespace MacFJA\RediSearch\tests\Redis\Command;
 
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\PartialQuery;
-use RangeException;
+use MacFJA\RediSearch\Redis\Command\SugLen;
+use PHPUnit\Framework\TestCase;
 
-class Filter implements PartialQuery
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SugLen
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class SugLenTest extends TestCase
 {
-    /** @var string */
-    private $fieldName;
-
-    /** @var float */
-    private $min;
-
-    /** @var float */
-    private $max;
-
-    public function __construct(string $fieldName, float $min, float $max)
+    public function testGetId(): void
     {
-        DataHelper::assert($min < $max, new RangeException('Maximum can\'t be inferior to minimum'));
-        $this->fieldName = $fieldName;
-        $this->min = $min;
-        $this->max = $max;
+        $command = new SugLen();
+        static::assertSame('FT.SUGLEN', $command->getId());
     }
 
-    public function getQueryParts(): array
+    public function testFullOption(): void
     {
-        return ['FILTER', $this->fieldName, $this->min, $this->max];
+        $command = new SugLen();
+        $command
+            ->setDictionary('ac')
+            ;
+
+        static::assertSame([
+            'ac',
+        ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/SynDumpTest.php b/tests/Redis/Command/SynDumpTest.php
new file mode 100644
index 0000000..f3e24cc
--- /dev/null
+++ b/tests/Redis/Command/SynDumpTest.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\SynDump;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SynDump
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class SynDumpTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new SynDump();
+        static::assertSame('FT.SYNDUMP', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new SynDump();
+        $command
+            ->setIndex('idx')
+            ;
+
+        static::assertSame([
+            'idx',
+        ], $command->getArguments());
+    }
+}
diff --git a/src/Index/Builder/TagField.php b/tests/Redis/Command/SynUpdateTest.php
similarity index 52%
rename from src/Index/Builder/TagField.php
rename to tests/Redis/Command/SynUpdateTest.php
index 93a1d33..7490514 100644
--- a/src/Index/Builder/TagField.php
+++ b/tests/Redis/Command/SynUpdateTest.php
@@ -19,42 +19,42 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Index\Builder;
+namespace MacFJA\RediSearch\tests\Redis\Command;
 
-use MacFJA\RediSearch\Helper\DataHelper;
-use MacFJA\RediSearch\Helper\RedisHelper;
-use MacFJA\RediSearch\Index\Builder\Exception\MustBeSingleCharException;
-use function strlen;
+use MacFJA\RediSearch\Redis\Command\SynUpdate;
+use PHPUnit\Framework\TestCase;
 
-class TagField extends AbstractField
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\SynUpdate
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ *
+ * @internal
+ */
+class SynUpdateTest extends TestCase
 {
-    /** @var null|string */
-    private $separator;
-
-    public function __construct(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false)
-    {
-        DataHelper::assert(
-            null === $separator || 1 === strlen($separator),
-            new MustBeSingleCharException('Separator must be a single char')
-        );
-        parent::__construct($name, $sortable, $noIndex);
-        $this->separator = $separator;
-    }
-
-    public function getType(): string
-    {
-        return self::TYPE_TAG;
-    }
-
-    public function getSeparator(): ?string
+    public function testGetId(): void
     {
-        return $this->separator;
+        $command = new SynUpdate();
+        static::assertSame('FT.SYNUPDATE', $command->getId());
     }
 
-    protected function getAdditionalQueryParts(): array
+    public function testFullOption(): void
     {
-        return RedisHelper::buildQueryNotNull([], [
-            'SEPARATOR' => $this->separator,
-        ]);
+        $command = new SynUpdate();
+        $command
+            ->setIndex('idx')
+            ->setGroupId('blue')
+            ->setSkipInitialScan()
+            ->setTerms('blue', 'cyan', 'navy', 'teal')
+            ;
+
+        static::assertSame([
+            'idx', 'blue', 'SKIPINITIALSCAN', 'blue',  'cyan', 'navy', 'teal',
+        ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/TagValsTest.php b/tests/Redis/Command/TagValsTest.php
new file mode 100644
index 0000000..9df66f9
--- /dev/null
+++ b/tests/Redis/Command/TagValsTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\TagVals;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\TagVals
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class TagValsTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new TagVals();
+        static::assertSame('FT.TAGVALS', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $command = new TagVals();
+        $command
+            ->setIndex('idx')
+            ->setField('myTag')
+            ;
+
+        static::assertSame([
+            'idx', 'myTag',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/InitializerTest.php b/tests/Redis/InitializerTest.php
new file mode 100644
index 0000000..40bb653
--- /dev/null
+++ b/tests/Redis/InitializerTest.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Initializer;
+use Predis\Client;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Initializer
+ *
+ * @internal
+ */
+class InitializerTest extends \PHPUnit\Framework\TestCase
+{
+    /**
+     * @param array<string> $input
+     * @dataProvider dataProvider
+     */
+    public function testRediSearchVersion(array $input, ?string $expected): void
+    {
+        $client = $this->getMockBuilder(Client::class)
+            ->addMethods(['info'])
+            ->getMock()
+        ;
+        $client->method('info')->with('Modules')->willReturn(['Modules' => $input]);
+
+        static::assertSame($expected, Initializer::getRediSearchVersion($client));
+    }
+
+    /**
+     * @return Generator<array>
+     */
+    public function dataProvider(string $testName): Generator
+    {
+        if ('testRediSearchVersion' === $testName) {
+            yield [['name=search,ver=20005,api=1,filters=0,usedby=[],using=[],options=[]'], '2.0.5'];
+            yield [['name=search,ver=20000,api=1,filters=0,usedby=[],using=[],options=[]'], '2.0.0'];
+            yield [['name=search,ver=26512,api=1,filters=0,usedby=[],using=[],options=[]'], '2.65.12'];
+            yield [['name=search,ver=120569,api=1,filters=0,usedby=[],using=[],options=[]'], '12.5.69'];
+            yield [['name=search,ver=99999,api=1,filters=0,usedby=[],using=[],options=[]'], '9.99.99'];
+            yield [['name=json,ver=99999,api=1,filters=0,usedby=[],using=[],options=[]'], null];
+            yield [['name=json,ver=99999,api=1,filters=0,usedby=[],using=[],options=[]', 'name=search,ver=20005,api=1,filters=0,usedby=[],using=[],options=[]'], '2.0.5'];
+        }
+    }
+}
diff --git a/tests/Redis/Response/InfoResponseTest.php b/tests/Redis/Response/InfoResponseTest.php
new file mode 100644
index 0000000..61899e8
--- /dev/null
+++ b/tests/Redis/Response/InfoResponseTest.php
@@ -0,0 +1,235 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Response;
+
+use BadMethodCallException;
+use InvalidArgumentException;
+use MacFJA\RediSearch\Redis\Command\Info;
+use MacFJA\RediSearch\Redis\Response\InfoResponse;
+use PHPUnit\Framework\TestCase;
+use Predis\Response\Status;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand::parseResponse
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand::transformParsedResponse
+ * @covers \MacFJA\RediSearch\Redis\Command\Info
+ * @covers \MacFJA\RediSearch\Redis\Response\InfoResponse
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand::__construct
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ *
+ * @internal
+ */
+class InfoResponseTest extends TestCase
+{
+    public function testResponse(): void
+    {
+        $rawResponse = [
+            0 => new Status('index_name'),
+            1 => new Status('idxt'),
+            2 => new Status('index_options'),
+            3 => [],
+            4 => new Status('index_definition'),
+            5 => [
+                0 => new Status('key_type'),
+                1 => new Status('HASH'),
+                2 => new Status('prefixes'),
+                3 => [0 => new Status('test')],
+                4 => new Status('language_field'),
+                5 => new Status('__language'),
+                6 => new Status('default_score'),
+                7 => '1',
+                8 => new Status('score_field'),
+                9 => new Status('__score'),
+                10 => new Status('payload_field'),
+                11 => new Status('__payload'),
+            ],
+            6 => new Status('fields'),
+            7 => [
+                0 => [
+                    0 => new Status('text'),
+                    1 => new Status('type'),
+                    2 => new Status('TEXT'),
+                    3 => new Status('WEIGHT'),
+                    4 => '1',
+                ],
+            ],
+            8 => new Status('num_docs'),
+            9 => '4',
+            10 => new Status('max_doc_id'),
+            11 => '12',
+            12 => new Status('num_terms'),
+            13 => '4',
+            14 => new Status('num_records'),
+            15 => '30',
+            16 => new Status('inverted_sz_mb'),
+            17 => '0.000171661376953125',
+            18 => new Status('total_inverted_index_blocks'),
+            19 => '1801',
+            20 => new Status('offset_vectors_sz_mb'),
+            21 => '2.86102294921875e-05',
+            22 => new Status('doc_table_size_mb'),
+            23 => '0.000995635986328125',
+            24 => new Status('sortable_values_size_mb'),
+            25 => '0',
+            26 => new Status('key_table_size_mb'),
+            27 => '0.00012493133544921875',
+            28 => new Status('records_per_doc_avg'),
+            29 => '7.5',
+            30 => new Status('bytes_per_record_avg'),
+            31 => '6',
+            32 => new Status('offsets_per_term_avg'),
+            33 => '1',
+            34 => new Status('offset_bits_per_record_avg'),
+            35 => '8',
+            36 => new Status('hash_indexing_failures'),
+            37 => '0',
+            38 => new Status('indexing'),
+            39 => '0',
+            40 => new Status('percent_indexed'),
+            41 => '1',
+            42 => new Status('gc_stats'),
+            43 => [
+                0 => new Status('bytes_collected'),
+                1 => '0',
+                2 => new Status('total_ms_run'),
+                3 => '0',
+                4 => new Status('total_cycles'),
+                5 => '0',
+                6 => new Status('average_cycle_time_ms'),
+                7 => '-nan',
+                8 => new Status('last_run_time_ms'),
+                9 => '0',
+                10 => new Status('gc_numeric_trees_missed'),
+                11 => '0',
+                12 => new Status('gc_blocks_denied'),
+                13 => '0',
+            ],
+            44 => new Status('cursor_stats'),
+            45 => [
+                0 => new Status('global_idle'),
+                1 => 63,
+                2 => new Status('global_total'),
+                3 => 63,
+                4 => new Status('index_capacity'),
+                5 => 128,
+                6 => new Status('index_total'),
+                7 => 63,
+            ],
+        ];
+        $command = new Info();
+        /** @var InfoResponse $parsed */
+        $parsed = $command->parseResponse($rawResponse);
+
+        static::assertSame('idxt', $parsed->getIndexName());
+        static::assertSame(4, $parsed->getNumDocs());
+        static::assertSame(12, $parsed->getMaxDocId());
+        static::assertSame(4, $parsed->getNumTerms());
+        static::assertSame(30, $parsed->getNumRecords());
+        static::assertSame(0.000171661376953125, $parsed->getInvertedSzMb());
+        static::assertSame(1801, $parsed->getTotalInvertedIndexBlocks());
+        static::assertSame(2.86102294921875e-05, $parsed->getOffsetVectorsSzMb());
+        static::assertSame(0.000995635986328125, $parsed->getDocTableSizeMb());
+        static::assertSame(0.0, $parsed->getSortableValuesSizeMb());
+        static::assertSame(0.00012493133544921875, $parsed->getKeyTableSizeMb());
+        static::assertSame(7.5, $parsed->getRecordsPerDocAvg());
+        static::assertSame(6.0, $parsed->getBytesPerRecordAvg());
+        static::assertSame(1.0, $parsed->getOffsetsPerTermAvg());
+        static::assertSame(8.0, $parsed->getOffsetBitsPerRecordAvg());
+        static::assertSame(0, $parsed->getHashIndexingFailures());
+        static::assertFalse($parsed->getIndexing());
+        static::assertSame(1.0, $parsed->getPercentIndexed());
+
+        $this->doTestGetFieldsAsOption($parsed);
+        $this->doTestGetIndexDefinition($parsed);
+    }
+
+    public function doTestGetFieldsAsOption(InfoResponse $parsed): void
+    {
+        $response = $parsed->getFieldsAsOption();
+
+        static::assertSame(['text', 'TEXT', 'WEIGHT', '1'], $response[0]->render());
+    }
+
+    public function doTestGetIndexDefinition(InfoResponse $parsed): void
+    {
+        static::assertEquals(
+            [
+                'key_type' => new Status('HASH'),
+                'prefixes' => [0 => new Status('test')],
+                'language_field' => new Status('__language'),
+                'default_score' => '1',
+                'score_field' => new Status('__score'),
+                'payload_field' => new Status('__payload'),
+            ],
+            $parsed->getIndexDefinition()
+        );
+
+        static::assertSame(
+            'HASH',
+            (string) $parsed->getIndexDefinition('key_type')
+        );
+        static::assertSame(
+            1.0,
+            (float) $parsed->getIndexDefinition('default_score')
+        );
+    }
+
+    public function testUnknownKey(): void
+    {
+        $response = new InfoResponse([]);
+        $this->expectException(InvalidArgumentException::class);
+
+        $response->getHelloWorld(); // @phpstan-ignore-line
+    }
+
+    public function testUnknownBadNaming1(): void
+    {
+        $response = new InfoResponse([]);
+        $this->expectException(BadMethodCallException::class);
+
+        $response->gethelloWorld(); // @phpstan-ignore-line
+    }
+
+    public function testUnknownBadNaming2(): void
+    {
+        $response = new InfoResponse([]);
+        $this->expectException(BadMethodCallException::class);
+
+        $response->gotHelloWorld(); // @phpstan-ignore-line
+    }
+
+    public function testNotANumber(): void
+    {
+        $response = new InfoResponse(['num_docs', '-nan']);
+
+        static::assertNull($response->getNumDocs());
+    }
+}
diff --git a/tests/Search/QueryBuilder/GeoFacetTest.php b/tests/Search/QueryBuilder/GeoFacetTest.php
deleted file mode 100644
index 23d836c..0000000
--- a/tests/Search/QueryBuilder/GeoFacetTest.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Search\QueryBuilder;
-
-use MacFJA\RediSearch\Search\Exception\UnknownUnitException;
-use MacFJA\RediSearch\Search\GeoFilter;
-use MacFJA\RediSearch\Search\QueryBuilder\GeoFacet;
-use MacFJA\RediSearch\Search\QueryBuilder\PartialQuery;
-use function PHPUnit\Framework\assertFalse;
-use function PHPUnit\Framework\assertSame;
-use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Search\QueryBuilder\GeoFacet
- * @covers ::__construct
- *
- * @uses \MacFJA\RediSearch\Helper\EscapeHelper
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\OrGroup
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\Word
- * @uses \MacFJA\RediSearch\Search\Exception\UnknownUnitException
- */
-class GeoFacetTest extends TestCase
-{
-    use Assertion;
-
-    /**
-     * @covers ::render
-     */
-    public function testNominal(): void
-    {
-        self::assertSameRender('@foobar:[-74.044502 40.689247 40.000000 km]', new GeoFacet('foobar', -74.044502, 40.689247, 40, GeoFilter::UNIT_KILOMETERS));
-    }
-
-    public function testValidUnits(): void
-    {
-        $this->expectNotToPerformAssertions();
-        new GeoFacet('foobar', -74.044502, 40.689247, 40, GeoFilter::UNIT_KILOMETERS);
-        new GeoFacet('foobar', -74.044502, 40.689247, 40, GeoFilter::UNIT_FEET);
-        new GeoFacet('foobar', -74.044502, 40.689247, 40, GeoFilter::UNIT_METERS);
-        new GeoFacet('foobar', -74.044502, 40.689247, 40, GeoFilter::UNIT_MILES);
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Search\Exception\UnknownUnitException
-     */
-    public function testUnknownUnit(): void
-    {
-        $this->expectException(UnknownUnitException::class);
-        $this->expectExceptionMessage('The unit "bar" is not valid.');
-
-        new GeoFacet('foo', 0, 0, 0, 'bar');
-    }
-
-    /**
-     * @covers ::includeSpace
-     */
-    public function testIncludeSpace(): void
-    {
-        assertFalse((new GeoFacet('foo', 0, 0, 0, 'm'))->includeSpace());
-    }
-
-    /**
-     * @covers ::priority
-     */
-    public function testPriority(): void
-    {
-        assertSame(PartialQuery::PRIORITY_NORMAL, (new GeoFacet('foo', 0, 0, 0, 'm'))->priority());
-    }
-}
diff --git a/tests/Search/QueryBuilder/NumericFacetTest.php b/tests/Search/QueryBuilder/NumericFacetTest.php
deleted file mode 100644
index 01ae0ca..0000000
--- a/tests/Search/QueryBuilder/NumericFacetTest.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Search\QueryBuilder;
-
-use MacFJA\RediSearch\Search\QueryBuilder\NumericFacet;
-use MacFJA\RediSearch\Search\QueryBuilder\PartialQuery;
-use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Search\QueryBuilder\NumericFacet
- * @covers ::__construct
- * @covers ::render
- *
- * @uses \MacFJA\RediSearch\Helper\EscapeHelper
- *
- * @see: https://oss.redislabs.com/redisearch/Query_Syntax/#mapping_common_sql_predicates_to_redisearch
- * WHERE num BETWEEN 10 AND 20	    @num:[10 20]
- * WHERE num >= 10	                @num:[10 +inf]
- * WHERE num > 10	                @num:[(10 +inf]
- * WHERE num < 10	                @num:[-inf (10]
- * WHERE num <= 10	                @num:[-inf 10]
- * WHERE num < 10 OR num > 20	    @num:[-inf (10] | @num:[(20 +inf]
- */
-class NumericFacetTest extends TestCase
-{
-    use Assertion;
-
-    public function testNominal(): void
-    {
-        self::assertSameRender('@foo:[0 10]', new NumericFacet('foo', 0, 10));
-        self::assertSameRender('@foo:[0 10]', new NumericFacet('foo', 0, 10, true, true));
-        self::assertSameRender('@foo:[(0 10]', new NumericFacet('foo', 0, 10, false));
-        self::assertSameRender('@foo:[0 (10]', new NumericFacet('foo', 0, 10, true, false));
-        self::assertSameRender('@foo:[(0 (10]', new NumericFacet('foo', 0, 10, false, false));
-    }
-
-    /**
-     * @covers ::greaterThan
-     */
-    public function testGreaterThan()
-    {
-        $facet = NumericFacet::greaterThan('num', 10);
-        self::assertSameRender('@num:[(10 +inf]', $facet);
-    }
-
-    /**
-     * @covers ::greaterThanOrEquals
-     */
-    public function testGreaterThanOrEquals()
-    {
-        $facet = NumericFacet::greaterThanOrEquals('num', 10);
-        self::assertSameRender('@num:[10 +inf]', $facet);
-    }
-
-    /**
-     * @covers ::lessThan
-     */
-    public function testLessThan()
-    {
-        $facet = NumericFacet::lessThan('num', 10);
-        self::assertSameRender('@num:[-inf (10]', $facet);
-    }
-
-    /**
-     * @covers ::lessThanOrEquals
-     */
-    public function testLessThanOrEquals()
-    {
-        $facet = NumericFacet::lessThanOrEquals('num', 10);
-        self::assertSameRender('@num:[-inf 10]', $facet);
-    }
-
-    /**
-     * @covers ::equalsTo
-     */
-    public function testEqualsTo()
-    {
-        $facet = NumericFacet::equalsTo('num', 10);
-        self::assertSameRender('@num:[10 10]', $facet);
-    }
-
-    /**
-     * @covers ::includeSpace
-     */
-    public function testIncludeSpace(): void
-    {
-        self::assertFalse((new NumericFacet('foobar', null, null))->includeSpace());
-    }
-
-    /**
-     * @covers ::priority
-     */
-    public function testPriority(): void
-    {
-        self::assertSame(PartialQuery::PRIORITY_NORMAL, (new NumericFacet('foobar', null, null))->priority());
-    }
-}
diff --git a/tests/Search/QueryBuilder/TagFacetTest.php b/tests/Search/QueryBuilder/TagFacetTest.php
deleted file mode 100644
index 91cecf8..0000000
--- a/tests/Search/QueryBuilder/TagFacetTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Search\QueryBuilder;
-
-use MacFJA\RediSearch\Search\Exception\NotEnoughTermsException;
-use MacFJA\RediSearch\Search\QueryBuilder\PartialQuery;
-use MacFJA\RediSearch\Search\QueryBuilder\TagFacet;
-use function PHPUnit\Framework\assertFalse;
-use function PHPUnit\Framework\assertSame;
-use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Search\QueryBuilder\TagFacet
- * @covers ::__construct
- *
- * @uses \MacFJA\RediSearch\Helper\EscapeHelper
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\OrGroup
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\Word
- */
-class TagFacetTest extends TestCase
-{
-    use Assertion;
-
-    /**
-     * @covers ::render
-     */
-    public function testNominal(): void
-    {
-        self::assertSameRender('@foobar:{hello}', new TagFacet('foobar', 'hello'));
-        self::assertSameRender('@foobar:{hello|world}', new TagFacet('foobar', 'hello', 'world'));
-        self::assertSameRender('@foobar:{hello world}', new TagFacet('foobar', 'hello world'));
-        self::assertSameRender('@foobar:{hello\|world}', new TagFacet('foobar', 'hello|world'));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Search\Exception\NotEnoughTermsException
-     *
-     * @uses \MacFJA\RediSearch\Words\Exception\NotEnoughTermsException
-     */
-    public function testNotEnoughValues(): void
-    {
-        $this->expectException(NotEnoughTermsException::class);
-        $this->expectExceptionMessage('You must have at least one term');
-
-        new TagFacet('foobar');
-    }
-
-    /**
-     * @covers ::includeSpace
-     */
-    public function testIncludeSpace(): void
-    {
-        assertFalse((new TagFacet('foo', 'bar'))->includeSpace());
-    }
-
-    /**
-     * @covers ::priority
-     */
-    public function testPriority(): void
-    {
-        assertSame(PartialQuery::PRIORITY_NORMAL, (new TagFacet('foo', 'bar'))->priority());
-    }
-}
diff --git a/tests/Search/QueryBuilder/TextFacetTest.php b/tests/Search/QueryBuilder/TextFacetTest.php
deleted file mode 100644
index ffcc3d5..0000000
--- a/tests/Search/QueryBuilder/TextFacetTest.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * Copyright MacFJA
- *
- * 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.
- */
-
-namespace Tests\MacFJA\RediSearch\Search\QueryBuilder;
-
-use MacFJA\RediSearch\Search\Exception\NotEnoughTermsException;
-use MacFJA\RediSearch\Search\QueryBuilder\PartialQuery;
-use MacFJA\RediSearch\Search\QueryBuilder\TextFacet;
-use function PHPUnit\Framework\assertFalse;
-use function PHPUnit\Framework\assertSame;
-use PHPUnit\Framework\TestCase;
-use Tests\MacFJA\RediSearch\support\Assertion;
-
-/**
- * @coversDefaultClass \MacFJA\RediSearch\Search\QueryBuilder\TextFacet
- * @covers ::__construct
- *
- * @uses \MacFJA\RediSearch\Helper\EscapeHelper
- * @uses \MacFJA\RediSearch\Helper\DataHelper
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\ExactMatch
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\OrGroup
- * @uses \MacFJA\RediSearch\Search\QueryBuilder\Word
- */
-class TextFacetTest extends TestCase
-{
-    use Assertion;
-
-    /**
-     * @covers ::render
-     */
-    public function testNominal(): void
-    {
-        self::assertSameRender('@foobar:hello', new TextFacet('foobar', 'hello'));
-        self::assertSameRender('@foobar:(hello|world)', new TextFacet('foobar', 'hello', 'world'));
-        self::assertSameRender('@foobar:(hello world)', new TextFacet('foobar', 'hello world'));
-        self::assertSameRender('@foobar:hello\|world', new TextFacet('foobar', 'hello|world'));
-    }
-
-    /**
-     * @covers \MacFJA\RediSearch\Search\Exception\NotEnoughTermsException
-     *
-     * @uses \MacFJA\RediSearch\Words\Exception\NotEnoughTermsException
-     */
-    public function testNotEnoughValues(): void
-    {
-        $this->expectException(NotEnoughTermsException::class);
-        $this->expectExceptionMessage('You must have at least one term');
-
-        new TextFacet('foobar');
-    }
-
-    /**
-     * @covers ::includeSpace
-     */
-    public function testIncludeSpace(): void
-    {
-        assertFalse((new TextFacet('foo', 'bar'))->includeSpace());
-    }
-
-    /**
-     * @covers ::priority
-     */
-    public function testPriority(): void
-    {
-        assertSame(PartialQuery::PRIORITY_NORMAL, (new TextFacet('foo', 'bar'))->priority());
-    }
-}
diff --git a/tests/fixtures/Helper/EscaperHelperFixture.csv b/tests/fixtures/Helper/EscaperHelperFixture.csv
deleted file mode 100644
index cf2aedb..0000000
--- a/tests/fixtures/Helper/EscaperHelperFixture.csv
+++ /dev/null
@@ -1,32 +0,0 @@
-"input","field name","fuzzy","exact match","optional","negation","word"
-"","'@' + tranformed + ':'","'%' + tranformed + '%'","'""' + tranformed + '""'","'~' + transformed","'-' + transformed","transformed"
-"foobar","foobar","foobar","foobar","foobar","foobar","foobar"
-"""foobar","\""foobar","\""foobar","\""foobar","\""foobar","\""foobar","\""foobar"
-"foobar""","foobar\""","foobar\""","foobar\""","foobar\""","foobar\""","foobar\"""
-"""foobar""","\""foobar\""","\""foobar\""","\""foobar\""","\""foobar\""","\""foobar\""","\""foobar\"""
-"@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar"
-"foobar:","foobar\:","foobar\:","foobar\:","foobar\:","foobar\:","foobar\:"
-"@foobar:","\@foobar\:","\@foobar\:","\@foobar\:","\@foobar\:","\@foobar\:","\@foobar\:"
-"-foobar","\-foobar","\-foobar","\-foobar","\-foobar","\-foobar","\-foobar"
-"foo@bar","foo\@bar","foo\@bar","foo\@bar","foo\@bar","foo\@bar","foo\@bar"
-"foo-bar","foo\-bar","foo\-bar","foo\-bar","foo\-bar","foo\-bar","foo\-bar"
-"foo.bar","foo\.bar","foo\.bar","foo\.bar","foo\.bar","foo\.bar","foo\.bar"
-"foobar-","foobar\-","foobar\-","foobar\-","foobar\-","foobar\-","foobar\-"
-"foo,bar","foo\,bar","foo\,bar","foo\,bar","foo\,bar","foo\,bar","foo\,bar"
-"\@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar"
-"foo""bar","foo\""bar","foo\""bar","foo\""bar","foo\""bar","foo\""bar","foo\""bar"
-"%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar"
-"foo%bar","foo\%bar","foo\%bar","foo\%bar","foo\%bar","foo\%bar","foo\%bar"
-"%foobar%","\%foobar\%","\%foobar\%","\%foobar\%","\%foobar\%","\%foobar\%","\%foobar\%"
-"\%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar"
-"\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar"
-"foo=bar","foo\=bar","foo\=bar","foo\=bar","foo\=bar","foo\=bar","foo\=bar"
-"~foobar","\~foobar","\~foobar","\~foobar","\~foobar","\~foobar","\~foobar"
-"foobar*","foobar\*","foobar\*","foobar\*","foobar\*","foobar\*","foobar\*"
-"foo|bar","foo\|bar","foo\|bar","foo\|bar","foo\|bar","foo\|bar","foo\|bar"
-"foo(bar","foo\(bar","foo\(bar","foo\(bar","foo\(bar","foo\(bar","foo\(bar"
-"foo{bar","foo\{bar","foo\{bar","foo\{bar","foo\{bar","foo\{bar","foo\{bar"
-"foo[bar","foo\[bar","foo\[bar","foo\[bar","foo\[bar","foo\[bar","foo\[bar"
-"foo$bar","foo\$bar","foo\$bar","foo\$bar","foo\$bar","foo\$bar","foo\$bar"
-"-29","\-29","-29","-29","-29","\-29","-29"
-"29","29","29","29","29","\29","29"
\ No newline at end of file
diff --git a/src/Words/SynonymResult.php b/tests/fixtures/Redis/Command/FakeAbstractCommand.php
similarity index 70%
rename from src/Words/SynonymResult.php
rename to tests/fixtures/Redis/Command/FakeAbstractCommand.php
index a22fff0..9f184c9 100644
--- a/src/Words/SynonymResult.php
+++ b/tests/fixtures/Redis/Command/FakeAbstractCommand.php
@@ -19,38 +19,34 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch\Words;
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command;
 
-use MacFJA\RediSearch\Helper\DataHelper;
-
-class SynonymResult
+/**
+ * @internal
+ */
+class FakeAbstractCommand extends \MacFJA\RediSearch\Redis\Command\AbstractCommand
 {
-    /** @var string */
-    private $for;
-
     /** @var array<string> */
-    private $synonyms = [];
+    private $requiredOptions;
 
-    /**
-     * @param array<string> $synonyms
-     */
-    public function __construct(string $for, array $synonyms)
+    public function getId()
     {
-        DataHelper::assertArrayOf($synonyms, 'string');
-        $this->for = $for;
-        $this->synonyms = $synonyms;
+        return '';
     }
 
-    public function getFor(): string
+    /**
+     * @param array<string> $requiredOptions
+     */
+    public function setRequiredOptions(array $requiredOptions): void
     {
-        return $this->for;
+        $this->requiredOptions = $requiredOptions;
     }
 
     /**
      * @return array<string>
      */
-    public function getSynonyms(): array
+    protected function getRequiredOptions(): array
     {
-        return $this->synonyms;
+        return $this->requiredOptions;
     }
 }
diff --git a/src/Builder.php b/tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass1.php
similarity index 86%
rename from src/Builder.php
rename to tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass1.php
index 3428760..9bb4b97 100644
--- a/src/Builder.php
+++ b/tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass1.php
@@ -19,14 +19,11 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RediSearch;
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command;
 
-interface Builder
-{
-    public function reset(): self;
+use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
 
-    /**
-     * @return mixed
-     */
-    public function execute();
+class FakeAddFieldOptionTraitClass1
+{
+    use AddFieldOptionTrait;
 }
diff --git a/alias/Aggregate/Reducer.php b/tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass2.php
similarity index 84%
rename from alias/Aggregate/Reducer.php
rename to tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass2.php
index 2f97d60..c4d8082 100644
--- a/alias/Aggregate/Reducer.php
+++ b/tests/fixtures/Redis/Command/FakeAddFieldOptionTraitClass2.php
@@ -19,9 +19,11 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
-namespace MacFJA\RedisSearch\Aggregate;
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command;
 
-/** @deprecated Bad namespace naming, use \MacFJA\RediSearch\Aggregate\Reducer */
-final class Reducer extends \MacFJA\RediSearch\Aggregate\Reducer
+use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
+
+class FakeAddFieldOptionTraitClass2 extends FakeAbstractCommand
 {
+    use AddFieldOptionTrait;
 }
diff --git a/tests/fixtures/Redis/Command/Option/FakeAbstractCommandOption.php b/tests/fixtures/Redis/Command/Option/FakeAbstractCommandOption.php
new file mode 100644
index 0000000..c769db2
--- /dev/null
+++ b/tests/fixtures/Redis/Command/Option/FakeAbstractCommandOption.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command\Option;
+
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+
+/**
+ * @internal
+ */
+class FakeAbstractCommandOption extends AbstractCommandOption
+{
+    public function isValid(): bool
+    {
+        return true;
+    }
+
+    /**
+     * @phpstan-return null
+     */
+    public function getOptionData()
+    {
+        return null;
+    }
+
+    protected function doRender(?string $version): array
+    {
+        return [];
+    }
+}

From d29e95e99fe2d8f722d4eb7f50a40376070bb4b7 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 14:42:38 +0200
Subject: [PATCH 02/60] Improve query attribute float handling

---
 composer.json                                 |   1 +
 composer.lock                                 | 532 ++++++++++--------
 src/Query/Builder/QueryElementAttribute.php   |   4 +-
 .../Builder/QueryElementAttributeTest.php     |  64 +++
 tests/Query/BuilderTest.php                   |  12 +-
 5 files changed, 357 insertions(+), 256 deletions(-)
 create mode 100644 tests/Query/Builder/QueryElementAttributeTest.php

diff --git a/composer.json b/composer.json
index bc15b70..898e756 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
     ],
     "require": {
         "php": "^7.2",
+        "ext-intl": "*",
         "composer/semver": "^3.2",
         "predis/predis": "^1.1",
         "respect/validation": "^2.0"
diff --git a/composer.lock b/composer.lock
index 91087af..9c486a2 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b00accba6f256ea837d7e330fa104da9",
+    "content-hash": "9ed68d6965de036262b2505f9aa3eadf",
     "packages": [
         {
             "name": "composer/semver",
@@ -89,16 +89,16 @@
         },
         {
             "name": "predis/predis",
-            "version": "v1.1.7",
+            "version": "v1.1.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/predis/predis.git",
-                "reference": "b240daa106d4e02f0c5b7079b41e31ddf66fddf8"
+                "reference": "cf5c118a077fbab8b9af1482c20952173125c041"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/predis/predis/zipball/b240daa106d4e02f0c5b7079b41e31ddf66fddf8",
-                "reference": "b240daa106d4e02f0c5b7079b41e31ddf66fddf8",
+                "url": "https://api.github.com/repos/predis/predis/zipball/cf5c118a077fbab8b9af1482c20952173125c041",
+                "reference": "cf5c118a077fbab8b9af1482c20952173125c041",
                 "shasum": ""
             },
             "require": {
@@ -143,7 +143,7 @@
             ],
             "support": {
                 "issues": "https://github.com/predis/predis/issues",
-                "source": "https://github.com/predis/predis/tree/v1.1.7"
+                "source": "https://github.com/predis/predis/tree/v1.1.8"
             },
             "funding": [
                 {
@@ -151,7 +151,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-04-04T19:34:46+00:00"
+            "time": "2021-09-29T17:48:39+00:00"
         },
         {
             "name": "respect/stringifier",
@@ -278,16 +278,16 @@
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.23.0",
+            "version": "v1.23.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
+                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
-                "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
+                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
                 "shasum": ""
             },
             "require": {
@@ -338,7 +338,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0"
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
             },
             "funding": [
                 {
@@ -354,33 +354,33 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-27T09:27:20+00:00"
+            "time": "2021-05-27T12:26:48+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "amphp/amp",
-            "version": "v2.5.2",
+            "version": "v2.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/amphp/amp.git",
-                "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9"
+                "reference": "c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9",
-                "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9",
+                "url": "https://api.github.com/repos/amphp/amp/zipball/c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae",
+                "reference": "c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7"
+                "php": ">=7.1"
             },
             "require-dev": {
                 "amphp/php-cs-fixer-config": "dev-master",
                 "amphp/phpunit-util": "^1",
                 "ext-json": "*",
                 "jetbrains/phpstorm-stubs": "^2019.3",
-                "phpunit/phpunit": "^6.0.9 | ^7",
+                "phpunit/phpunit": "^7 | ^8 | ^9",
                 "psalm/phar": "^3.11@dev",
                 "react/promise": "^2"
             },
@@ -437,7 +437,7 @@
             "support": {
                 "irc": "irc://irc.freenode.org/amphp",
                 "issues": "https://github.com/amphp/amp/issues",
-                "source": "https://github.com/amphp/amp/tree/v2.5.2"
+                "source": "https://github.com/amphp/amp/tree/v2.6.1"
             },
             "funding": [
                 {
@@ -445,7 +445,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-01-10T17:06:37+00:00"
+            "time": "2021-09-23T18:43:08+00:00"
         },
         {
             "name": "amphp/byte-stream",
@@ -526,16 +526,16 @@
         },
         {
             "name": "composer/package-versions-deprecated",
-            "version": "1.11.99.2",
+            "version": "1.11.99.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/package-versions-deprecated.git",
-                "reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c"
+                "reference": "b174585d1fe49ceed21928a945138948cb394600"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c6522afe5540d5fc46675043d3ed5a45a740b27c",
-                "reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c",
+                "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600",
+                "reference": "b174585d1fe49ceed21928a945138948cb394600",
                 "shasum": ""
             },
             "require": {
@@ -579,7 +579,7 @@
             "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
             "support": {
                 "issues": "https://github.com/composer/package-versions-deprecated/issues",
-                "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.2"
+                "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4"
             },
             "funding": [
                 {
@@ -595,25 +595,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-24T07:46:03+00:00"
+            "time": "2021-09-13T08:41:34+00:00"
         },
         {
             "name": "composer/xdebug-handler",
-            "version": "2.0.1",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/xdebug-handler.git",
-                "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496"
+                "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496",
-                "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496",
+                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339",
+                "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.3.2 || ^7.0 || ^8.0",
-                "psr/log": "^1.0"
+                "psr/log": "^1 || ^2 || ^3"
             },
             "require-dev": {
                 "phpstan/phpstan": "^0.12.55",
@@ -643,7 +643,7 @@
             "support": {
                 "irc": "irc://irc.freenode.org/composer",
                 "issues": "https://github.com/composer/xdebug-handler/issues",
-                "source": "https://github.com/composer/xdebug-handler/tree/2.0.1"
+                "source": "https://github.com/composer/xdebug-handler/tree/2.0.2"
             },
             "funding": [
                 {
@@ -659,7 +659,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-05T19:37:51+00:00"
+            "time": "2021-07-31T17:03:58+00:00"
         },
         {
             "name": "dnoegel/php-xdg-base-dir",
@@ -700,16 +700,16 @@
         },
         {
             "name": "doctrine/annotations",
-            "version": "1.13.1",
+            "version": "1.13.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/annotations.git",
-                "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f"
+                "reference": "5b668aef16090008790395c02c893b1ba13f7e08"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f",
-                "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f",
+                "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08",
+                "reference": "5b668aef16090008790395c02c893b1ba13f7e08",
                 "shasum": ""
             },
             "require": {
@@ -766,9 +766,9 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/annotations/issues",
-                "source": "https://github.com/doctrine/annotations/tree/1.13.1"
+                "source": "https://github.com/doctrine/annotations/tree/1.13.2"
             },
-            "time": "2021-05-16T18:07:53+00:00"
+            "time": "2021-08-05T19:00:23+00:00"
         },
         {
             "name": "doctrine/instantiator",
@@ -1308,16 +1308,16 @@
         },
         {
             "name": "friendsofphp/php-cs-fixer",
-            "version": "v3.0.0",
+            "version": "v3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
-                "reference": "c15377bdfa8d1ecf186f1deadec39c89984e1167"
+                "reference": "cf4cedb9e8991c2daa94a756176d81bf487e4c4b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/c15377bdfa8d1ecf186f1deadec39c89984e1167",
-                "reference": "c15377bdfa8d1ecf186f1deadec39c89984e1167",
+                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/cf4cedb9e8991c2daa94a756176d81bf487e4c4b",
+                "reference": "cf4cedb9e8991c2daa94a756176d81bf487e4c4b",
                 "shasum": ""
             },
             "require": {
@@ -1333,7 +1333,8 @@
                 "symfony/filesystem": "^4.4.20 || ^5.0",
                 "symfony/finder": "^4.4.20 || ^5.0",
                 "symfony/options-resolver": "^4.4.20 || ^5.0",
-                "symfony/polyfill-php72": "^1.22",
+                "symfony/polyfill-php72": "^1.23",
+                "symfony/polyfill-php81": "^1.23",
                 "symfony/process": "^4.4.20 || ^5.0",
                 "symfony/stopwatch": "^4.4.20 || ^5.0"
             },
@@ -1384,7 +1385,7 @@
             "description": "A tool to automatically fix PHP code style",
             "support": {
                 "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
-                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.0.0"
+                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.1.0"
             },
             "funding": [
                 {
@@ -1392,7 +1393,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-05-03T21:51:58+00:00"
+            "time": "2021-08-29T20:16:20+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
@@ -1655,16 +1656,16 @@
         },
         {
             "name": "justinrainbow/json-schema",
-            "version": "5.2.10",
+            "version": "5.2.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/justinrainbow/json-schema.git",
-                "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b"
+                "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
-                "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+                "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa",
+                "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa",
                 "shasum": ""
             },
             "require": {
@@ -1719,9 +1720,9 @@
             ],
             "support": {
                 "issues": "https://github.com/justinrainbow/json-schema/issues",
-                "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10"
+                "source": "https://github.com/justinrainbow/json-schema/tree/5.2.11"
             },
-            "time": "2020-05-27T16:41:55+00:00"
+            "time": "2021-07-22T09:24:00+00:00"
         },
         {
             "name": "league/climate",
@@ -1959,16 +1960,16 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v4.11.0",
+            "version": "v4.13.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94"
+                "reference": "50953a2691a922aa1769461637869a0a2faa3f53"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/fe14cf3672a149364fb66dfe11bf6549af899f94",
-                "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53",
+                "reference": "50953a2691a922aa1769461637869a0a2faa3f53",
                 "shasum": ""
             },
             "require": {
@@ -2009,9 +2010,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.11.0"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0"
             },
-            "time": "2021-07-03T13:36:55+00:00"
+            "time": "2021-09-20T12:20:58+00:00"
         },
         {
             "name": "openlss/lib-array2xml",
@@ -2068,16 +2069,16 @@
         },
         {
             "name": "pdepend/pdepend",
-            "version": "2.9.1",
+            "version": "2.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/pdepend/pdepend.git",
-                "reference": "1632f0cee84512ffd6dde71e58536b3b06528c41"
+                "reference": "1fd30f4352b630ad53fec3fd5e8b8ba760f85596"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1632f0cee84512ffd6dde71e58536b3b06528c41",
-                "reference": "1632f0cee84512ffd6dde71e58536b3b06528c41",
+                "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1fd30f4352b630ad53fec3fd5e8b8ba760f85596",
+                "reference": "1fd30f4352b630ad53fec3fd5e8b8ba760f85596",
                 "shasum": ""
             },
             "require": {
@@ -2113,7 +2114,7 @@
             "description": "Official version of pdepend to be handled with Composer",
             "support": {
                 "issues": "https://github.com/pdepend/pdepend/issues",
-                "source": "https://github.com/pdepend/pdepend/tree/2.9.1"
+                "source": "https://github.com/pdepend/pdepend/tree/2.10.0"
             },
             "funding": [
                 {
@@ -2121,20 +2122,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-04-15T21:36:28+00:00"
+            "time": "2021-07-20T09:56:09+00:00"
         },
         {
             "name": "phar-io/manifest",
-            "version": "2.0.1",
+            "version": "2.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/manifest.git",
-                "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
+                "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
-                "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
+                "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
                 "shasum": ""
             },
             "require": {
@@ -2179,9 +2180,9 @@
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
             "support": {
                 "issues": "https://github.com/phar-io/manifest/issues",
-                "source": "https://github.com/phar-io/manifest/tree/master"
+                "source": "https://github.com/phar-io/manifest/tree/2.0.3"
             },
-            "time": "2020-06-27T14:33:11+00:00"
+            "time": "2021-07-20T11:28:43+00:00"
         },
         {
             "name": "phar-io/version",
@@ -2288,16 +2289,16 @@
         },
         {
             "name": "php-parallel-lint/php-parallel-lint",
-            "version": "v1.3.0",
+            "version": "v1.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git",
-                "reference": "772a954e5f119f6f5871d015b23eabed8cbdadfb"
+                "reference": "761f3806e30239b5fcd90a0a45d41dc2138de192"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/772a954e5f119f6f5871d015b23eabed8cbdadfb",
-                "reference": "772a954e5f119f6f5871d015b23eabed8cbdadfb",
+                "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/761f3806e30239b5fcd90a0a45d41dc2138de192",
+                "reference": "761f3806e30239b5fcd90a0a45d41dc2138de192",
                 "shasum": ""
             },
             "require": {
@@ -2311,7 +2312,7 @@
             "require-dev": {
                 "nette/tester": "^1.3 || ^2.0",
                 "php-parallel-lint/php-console-highlighter": "~0.3",
-                "squizlabs/php_codesniffer": "^3.5"
+                "squizlabs/php_codesniffer": "^3.6"
             },
             "suggest": {
                 "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet"
@@ -2339,9 +2340,9 @@
             "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
             "support": {
                 "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues",
-                "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.3.0"
+                "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.3.1"
             },
-            "time": "2021-04-07T14:42:48+00:00"
+            "time": "2021-08-13T05:35:13+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
@@ -2454,16 +2455,16 @@
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "1.4.0",
+            "version": "1.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+                "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
-                "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae",
+                "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae",
                 "shasum": ""
             },
             "require": {
@@ -2471,7 +2472,8 @@
                 "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
-                "ext-tokenizer": "*"
+                "ext-tokenizer": "*",
+                "psalm/phar": "^4.8"
             },
             "type": "library",
             "extra": {
@@ -2497,28 +2499,28 @@
             "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
             "support": {
                 "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
-                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
+                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1"
             },
-            "time": "2020-09-17T18:55:26+00:00"
+            "time": "2021-10-02T14:08:47+00:00"
         },
         {
             "name": "phpmd/phpmd",
-            "version": "2.10.1",
+            "version": "2.10.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpmd/phpmd.git",
-                "reference": "bd5ef43d1dcaf7272605027c959c1c5ff3761f7a"
+                "reference": "1bc74db7cf834662d83abebae265be11bb2eec3a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpmd/phpmd/zipball/bd5ef43d1dcaf7272605027c959c1c5ff3761f7a",
-                "reference": "bd5ef43d1dcaf7272605027c959c1c5ff3761f7a",
+                "url": "https://api.github.com/repos/phpmd/phpmd/zipball/1bc74db7cf834662d83abebae265be11bb2eec3a",
+                "reference": "1bc74db7cf834662d83abebae265be11bb2eec3a",
                 "shasum": ""
             },
             "require": {
                 "composer/xdebug-handler": "^1.0 || ^2.0",
                 "ext-xml": "*",
-                "pdepend/pdepend": "^2.9.1",
+                "pdepend/pdepend": "^2.10.0",
                 "php": ">=5.3.9"
             },
             "require-dev": {
@@ -2574,7 +2576,7 @@
             "support": {
                 "irc": "irc://irc.freenode.org/phpmd",
                 "issues": "https://github.com/phpmd/phpmd/issues",
-                "source": "https://github.com/phpmd/phpmd/tree/2.10.1"
+                "source": "https://github.com/phpmd/phpmd/tree/2.10.2"
             },
             "funding": [
                 {
@@ -2582,37 +2584,37 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-11T17:16:16+00:00"
+            "time": "2021-07-22T09:56:23+00:00"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.13.0",
+            "version": "1.14.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
+                "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
-                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
+                "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.2",
-                "php": "^7.2 || ~8.0, <8.1",
+                "php": "^7.2 || ~8.0, <8.2",
                 "phpdocumentor/reflection-docblock": "^5.2",
                 "sebastian/comparator": "^3.0 || ^4.0",
                 "sebastian/recursion-context": "^3.0 || ^4.0"
             },
             "require-dev": {
-                "phpspec/phpspec": "^6.0",
+                "phpspec/phpspec": "^6.0 || ^7.0",
                 "phpunit/phpunit": "^8.0 || ^9.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.11.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
@@ -2647,22 +2649,22 @@
             ],
             "support": {
                 "issues": "https://github.com/phpspec/prophecy/issues",
-                "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
+                "source": "https://github.com/phpspec/prophecy/tree/1.14.0"
             },
-            "time": "2021-03-17T13:42:18+00:00"
+            "time": "2021-09-10T09:02:12+00:00"
         },
         {
             "name": "phpstan/phpstan",
-            "version": "0.12.92",
+            "version": "0.12.99",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "64d4c5dc8ea96972bc18432d137a330239a5d2b2"
+                "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/64d4c5dc8ea96972bc18432d137a330239a5d2b2",
-                "reference": "64d4c5dc8ea96972bc18432d137a330239a5d2b2",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4d40f1d759942f523be267a1bab6884f46ca3f7",
+                "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7",
                 "shasum": ""
             },
             "require": {
@@ -2693,7 +2695,7 @@
             "description": "PHPStan - PHP Static Analysis Tool",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/0.12.92"
+                "source": "https://github.com/phpstan/phpstan/tree/0.12.99"
             },
             "funding": [
                 {
@@ -2713,20 +2715,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-07-10T13:53:49+00:00"
+            "time": "2021-09-12T20:09:55+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "7.0.14",
+            "version": "7.0.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c"
+                "reference": "819f92bba8b001d4363065928088de22f25a3a48"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c",
-                "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/819f92bba8b001d4363065928088de22f25a3a48",
+                "reference": "819f92bba8b001d4363065928088de22f25a3a48",
                 "shasum": ""
             },
             "require": {
@@ -2735,7 +2737,7 @@
                 "php": ">=7.2",
                 "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^3.1.1 || ^4.0",
+                "phpunit/php-token-stream": "^3.1.3 || ^4.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
                 "sebastian/environment": "^4.2.2",
                 "sebastian/version": "^2.0.1",
@@ -2778,7 +2780,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15"
             },
             "funding": [
                 {
@@ -2786,20 +2788,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-12-02T13:39:03+00:00"
+            "time": "2021-07-26T12:20:09+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "2.0.3",
+            "version": "2.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357"
+                "reference": "28af674ff175d0768a5a978e6de83f697d4a7f05"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357",
-                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/28af674ff175d0768a5a978e6de83f697d4a7f05",
+                "reference": "28af674ff175d0768a5a978e6de83f697d4a7f05",
                 "shasum": ""
             },
             "require": {
@@ -2838,7 +2840,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
-                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3"
+                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.4"
             },
             "funding": [
                 {
@@ -2846,7 +2848,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-11-30T08:25:21+00:00"
+            "time": "2021-07-19T06:46:01+00:00"
         },
         {
             "name": "phpunit/php-text-template",
@@ -2954,16 +2956,16 @@
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "3.1.2",
+            "version": "3.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2"
+                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2",
-                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768",
+                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768",
                 "shasum": ""
             },
             "require": {
@@ -3001,7 +3003,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
-                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2"
+                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3"
             },
             "funding": [
                 {
@@ -3010,20 +3012,20 @@
                 }
             ],
             "abandoned": true,
-            "time": "2020-11-30T08:38:46+00:00"
+            "time": "2021-07-26T12:15:06+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "8.5.17",
+            "version": "8.5.21",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da"
+                "reference": "50a58a60b85947b0bee4c8ecfe0f4bbdcf20e984"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/79067856d85421c56d413bd238d4e2cd6b0e54da",
-                "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/50a58a60b85947b0bee4c8ecfe0f4bbdcf20e984",
+                "reference": "50a58a60b85947b0bee4c8ecfe0f4bbdcf20e984",
                 "shasum": ""
             },
             "require": {
@@ -3035,12 +3037,12 @@
                 "ext-xml": "*",
                 "ext-xmlwriter": "*",
                 "myclabs/deep-copy": "^1.10.0",
-                "phar-io/manifest": "^2.0.1",
+                "phar-io/manifest": "^2.0.3",
                 "phar-io/version": "^3.0.2",
                 "php": ">=7.2",
                 "phpspec/prophecy": "^1.10.3",
                 "phpunit/php-code-coverage": "^7.0.12",
-                "phpunit/php-file-iterator": "^2.0.2",
+                "phpunit/php-file-iterator": "^2.0.4",
                 "phpunit/php-text-template": "^1.2.1",
                 "phpunit/php-timer": "^2.1.2",
                 "sebastian/comparator": "^3.0.2",
@@ -3095,7 +3097,7 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.17"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.21"
             },
             "funding": [
                 {
@@ -3107,7 +3109,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2021-06-23T05:12:43+00:00"
+            "time": "2021-09-25T07:37:20+00:00"
         },
         {
             "name": "psr/cache",
@@ -3359,17 +3361,18 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/Roave/SecurityAdvisories.git",
-                "reference": "1a08d0c7ab47e57fad0d254951a615f07445e91f"
+                "reference": "0488e161600117fc3a0d72397dad154729002f54"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/1a08d0c7ab47e57fad0d254951a615f07445e91f",
-                "reference": "1a08d0c7ab47e57fad0d254951a615f07445e91f",
+                "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0488e161600117fc3a0d72397dad154729002f54",
+                "reference": "0488e161600117fc3a0d72397dad154729002f54",
                 "shasum": ""
             },
             "conflict": {
                 "3f/pygmentize": "<1.2",
                 "adodb/adodb-php": "<5.20.12",
+                "akaunting/akaunting": "<2.1.13",
                 "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
                 "amazing/media2click": ">=1,<1.3.3",
                 "amphp/artax": "<1.0.6|>=2,<2.0.6",
@@ -3381,19 +3384,23 @@
                 "bagisto/bagisto": "<0.1.5",
                 "barrelstrength/sprout-base-email": "<1.2.7",
                 "barrelstrength/sprout-forms": "<3.9",
-                "baserproject/basercms": "<4.4.5",
+                "baserproject/basercms": "<=4.5",
+                "billz/raspap-webgui": "<=2.6.6",
                 "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
                 "bolt/bolt": "<3.7.2",
                 "bolt/core": "<4.1.13",
                 "brightlocal/phpwhois": "<=4.2.5",
                 "buddypress/buddypress": "<5.1.2",
                 "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
+                "cachethq/cachet": "<2.5.1",
                 "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
                 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
                 "cartalyst/sentry": "<=2.1.6",
                 "centreon/centreon": "<20.10.7",
                 "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
+                "codeception/codeception": "<3.1.3|>=4,<4.1.22",
                 "codeigniter/framework": "<=3.0.6",
+                "codiad/codiad": "<=2.8.4",
                 "composer/composer": "<1.10.22|>=2-alpha.1,<2.0.13",
                 "contao-components/mediaelement": ">=2.14.2,<2.21.1",
                 "contao/core": ">=2,<3.5.39",
@@ -3414,11 +3421,12 @@
                 "doctrine/mongodb-odm": ">=1,<1.0.2",
                 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
                 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
-                "dolibarr/dolibarr": "<14",
+                "dolibarr/dolibarr": "<14|>= 3.3.beta1, < 13.0.2",
                 "dompdf/dompdf": ">=0.6,<0.6.2",
-                "drupal/core": ">=7,<7.80|>=8,<8.9.14|>=9,<9.0.12|>=9.1,<9.1.7",
-                "drupal/drupal": ">=7,<7.80|>=8,<8.9.14|>=9,<9.0.12|>=9.1,<9.1.7",
+                "drupal/core": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4",
+                "drupal/drupal": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4",
                 "dweeves/magmi": "<=0.7.24",
+                "ecodev/newsletter": "<=4",
                 "endroid/qr-code-bundle": "<3.4.2",
                 "enshrined/svg-sanitize": "<0.13.1",
                 "erusev/parsedown": "<1.7.2",
@@ -3427,26 +3435,27 @@
                 "ezsystems/ez-support-tools": ">=2.2,<2.2.3",
                 "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
                 "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
-                "ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
+                "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24",
                 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
                 "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
                 "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<=1.3.1",
-                "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<=1.3.1",
+                "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
                 "ezsystems/ezplatform-user": ">=1,<1.0.1",
                 "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<=7.5.15.1",
-                "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.2|>=2011,<2017.12.7.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.5.1",
+                "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.3.5.1",
                 "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
                 "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
                 "ezyang/htmlpurifier": "<4.1.1",
                 "facade/ignition": "<1.16.14|>=2,<2.4.2|>=2.5,<2.5.2",
                 "feehi/cms": "<=2.1.1",
+                "feehi/feehicms": "<=0.1.3",
                 "firebase/php-jwt": "<2",
                 "flarum/core": ">=1,<=1.0.1",
                 "flarum/sticky": ">=0.1-beta.14,<=0.1-beta.15",
                 "flarum/tags": "<=0.1-beta.13",
                 "fluidtypo3/vhs": "<5.1.1",
                 "fooman/tcpdf": "<6.2.22",
-                "forkcms/forkcms": "<5.8.3",
+                "forkcms/forkcms": "<=5.9.2",
                 "fossar/tcpdf-parser": "<6.2.22",
                 "francoisjacquet/rosariosis": "<6.5.1",
                 "friendsofsymfony/oauth2-php": "<1.3",
@@ -3455,20 +3464,25 @@
                 "friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
                 "froala/wysiwyg-editor": "<3.2.7",
                 "fuel/core": "<1.8.1",
-                "getgrav/grav": "<=1.7.10",
+                "getgrav/grav": "<1.7.21",
                 "getkirby/cms": "<=3.5.6",
                 "getkirby/panel": "<2.5.14",
+                "gilacms/gila": "<=1.11.4",
                 "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
                 "gree/jose": "<=2.2",
                 "gregwar/rst": "<1.0.3",
-                "grumpydictator/firefly-iii": "<5.5.13",
+                "grumpydictator/firefly-iii": "<5.6.1",
                 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
+                "helloxz/imgurl": "<=2.31",
+                "ibexa/post-install": "<=1.0.4",
+                "icecoder/icecoder": "<=8",
                 "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10",
                 "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4",
                 "illuminate/database": "<6.20.26|>=7,<8.40",
                 "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
                 "illuminate/view": ">=7,<7.1.2",
                 "impresscms/impresscms": "<=1.4.2",
+                "in2code/femanager": "<5.5.1|>=6,<6.3.1",
                 "intelliants/subrion": "<=4.2.1",
                 "ivankristianto/phpwhois": "<=4.3",
                 "james-heinrich/getid3": "<1.9.9",
@@ -3484,19 +3498,22 @@
                 "laravel/framework": "<6.20.26|>=7,<8.40",
                 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
                 "lavalite/cms": "<=5.8",
+                "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
                 "league/commonmark": "<0.18.3",
                 "league/flysystem": "<1.1.4|>=2,<2.1.1",
                 "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
                 "librenms/librenms": "<21.1",
                 "livewire/livewire": ">2.2.4,<2.2.6",
+                "lms/routes": "<2.1.1",
                 "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2",
                 "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
                 "magento/magento1ce": "<1.9.4.3",
                 "magento/magento1ee": ">=1,<1.14.4.3",
                 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
                 "marcwillmann/turn": "<0.3.3",
-                "mautic/core": "<3.3.2|= 2.13.1",
+                "mautic/core": "<4|= 2.13.1",
                 "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35",
+                "miniorange/miniorange-saml": "<1.4.3",
                 "mittwald/typo3_forum": "<1.2.1",
                 "monolog/monolog": ">=1.8,<1.12",
                 "moodle/moodle": "<3.5.17|>=3.7,<3.7.9|>=3.8,<3.8.8|>=3.9,<3.9.5|>=3.10,<3.10.2",
@@ -3515,11 +3532,12 @@
                 "october/cms": "= 1.1.1|= 1.0.471|= 1.0.469|>=1.0.319,<1.0.469",
                 "october/october": ">=1.0.319,<1.0.466",
                 "october/rain": "<1.0.472|>=1.1,<1.1.2",
+                "october/system": "<1.0.472|>=1.1.1,<1.1.5",
                 "onelogin/php-saml": "<2.10.4",
                 "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
                 "opencart/opencart": "<=3.0.3.2",
                 "openid/php-openid": "<2.3",
-                "openmage/magento-lts": "<=19.4.12|>=20,<=20.0.8",
+                "openmage/magento-lts": "<19.4.15|>=20,<20.0.13",
                 "orchid/platform": ">=9,<9.4.4",
                 "oro/crm": ">=1.7,<1.7.4",
                 "oro/platform": ">=1.7,<1.7.4",
@@ -3532,7 +3550,7 @@
                 "pear/archive_tar": "<1.4.14",
                 "personnummer/personnummer": "<3.0.2",
                 "phanan/koel": "<5.1.4",
-                "phpfastcache/phpfastcache": ">=5,<5.0.13",
+                "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7",
                 "phpmailer/phpmailer": "<6.5",
                 "phpmussel/phpmussel": ">=1,<1.6",
                 "phpmyadmin/phpmyadmin": "<4.9.6|>=5,<5.0.3",
@@ -3542,7 +3560,7 @@
                 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
                 "phpwhois/phpwhois": "<=4.2.5",
                 "phpxmlrpc/extras": "<0.6.1",
-                "pimcore/pimcore": "<10.0.7",
+                "pimcore/pimcore": "<10.1.3",
                 "pocketmine/pocketmine-mp": "<3.15.4",
                 "pressbooks/pressbooks": "<5.18",
                 "prestashop/autoupgrade": ">=4,<4.10.1",
@@ -3565,10 +3583,11 @@
                 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
                 "sensiolabs/connect": "<4.2.3",
                 "serluck/phpwhois": "<=4.2.6",
-                "shopware/core": "<=6.4.1",
-                "shopware/platform": "<=6.4.1",
+                "shopware/core": "<=6.4.3",
+                "shopware/platform": "<=6.4.3",
                 "shopware/production": "<=6.3.5.2",
-                "shopware/shopware": "<=5.6.9",
+                "shopware/shopware": "<5.6.10",
+                "showdoc/showdoc": "<=2.9.8",
                 "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
                 "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
                 "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
@@ -3595,6 +3614,7 @@
                 "ssddanbrown/bookstack": "<0.29.2",
                 "stormpath/sdk": ">=0,<9.9.99",
                 "studio-42/elfinder": "<2.1.59",
+                "subrion/cms": "<=4.2.1",
                 "sulu/sulu": "<1.6.41|>=2,<2.0.10|>=2.1,<2.1.1",
                 "swiftmailer/swiftmailer": ">=4,<5.4.5",
                 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
@@ -3605,6 +3625,7 @@
                 "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
                 "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4",
                 "symbiote/silverstripe-versionedfiles": "<=2.0.3",
+                "symfont/process": ">=0,<4",
                 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
                 "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
                 "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
@@ -3641,12 +3662,13 @@
                 "theonedemon/phpwhois": "<=4.2.5",
                 "titon/framework": ">=0,<9.9.99",
                 "topthink/think": "<=6.0.9",
+                "topthink/thinkphp": "<=3.2.3",
                 "tribalsystems/zenario": "<8.8.53370",
                 "truckersmp/phpwhois": "<=4.3.1",
                 "twig/twig": "<1.38|>=2,<2.7",
                 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.38|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
                 "typo3/cms-backend": ">=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
-                "typo3/cms-core": ">=6.2,<=6.2.56|>=7,<7.6.52|>=8,<8.7.41|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
+                "typo3/cms-core": ">=6.2,<=6.2.56|>=7,<=7.6.52|>=8,<=8.7.41|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
                 "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
                 "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
                 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
@@ -3655,9 +3677,12 @@
                 "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10",
                 "ua-parser/uap-php": "<3.8",
                 "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
+                "vanilla/safecurl": "<0.9.2",
                 "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
                 "vrana/adminer": "<4.7.9",
                 "wallabag/tcpdf": "<6.2.22",
+                "web-auth/webauthn-framework": ">=3.3,<3.3.4",
+                "webcoast/deferred-image-processing": "<1.0.2",
                 "wikimedia/parsoid": "<0.12.2",
                 "willdurand/js-translation-bundle": "<2.1.1",
                 "wp-cli/wp-cli": "<2.5",
@@ -3666,13 +3691,13 @@
                 "yiisoft/yii": ">=1.1.14,<1.1.15",
                 "yiisoft/yii2": "<2.0.38",
                 "yiisoft/yii2-bootstrap": "<2.0.4",
-                "yiisoft/yii2-dev": "<2.0.15",
+                "yiisoft/yii2-dev": "<2.0.43",
                 "yiisoft/yii2-elasticsearch": "<2.0.5",
                 "yiisoft/yii2-gii": "<2.0.4",
                 "yiisoft/yii2-jui": "<2.0.4",
                 "yiisoft/yii2-redis": "<2.0.8",
-                "yoast-seo-for-typo3/yoast_seo": "<7.2.1",
-                "yourls/yourls": "<1.7.4",
+                "yoast-seo-for-typo3/yoast_seo": "<7.2.3",
+                "yourls/yourls": "<=1.8.2",
                 "zendesk/zendesk_api_client_php": "<2.2.11",
                 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
                 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
@@ -3734,7 +3759,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-08-11T16:04:32+00:00"
+            "time": "2021-09-30T18:03:50+00:00"
         },
         {
             "name": "rskuipers/php-assumptions",
@@ -4673,22 +4698,23 @@
         },
         {
             "name": "symfony/config",
-            "version": "v4.4.26",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "1cb26cdb8a9834d8494cadd284602fa0647b73e5"
+                "reference": "d9ea72de055cd822e5228ff898e2aad2f52f76b0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/1cb26cdb8a9834d8494cadd284602fa0647b73e5",
-                "reference": "1cb26cdb8a9834d8494cadd284602fa0647b73e5",
+                "url": "https://api.github.com/repos/symfony/config/zipball/d9ea72de055cd822e5228ff898e2aad2f52f76b0",
+                "reference": "d9ea72de055cd822e5228ff898e2aad2f52f76b0",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
                 "symfony/filesystem": "^3.4|^4.0|^5.0",
                 "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-php80": "^1.16",
                 "symfony/polyfill-php81": "^1.22"
             },
             "conflict": {
@@ -4730,7 +4756,7 @@
             "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/config/tree/v4.4.26"
+                "source": "https://github.com/symfony/config/tree/v4.4.30"
             },
             "funding": [
                 {
@@ -4746,40 +4772,41 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-21T14:51:25+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v4.4.26",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "9aa1eb46c1b12fada74dc0c529e93d1ccef22576"
+                "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/9aa1eb46c1b12fada74dc0c529e93d1ccef22576",
-                "reference": "9aa1eb46c1b12fada74dc0c529e93d1ccef22576",
+                "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22",
+                "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
                 "symfony/polyfill-mbstring": "~1.0",
                 "symfony/polyfill-php73": "^1.8",
-                "symfony/polyfill-php80": "^1.15",
+                "symfony/polyfill-php80": "^1.16",
                 "symfony/service-contracts": "^1.1|^2"
             },
             "conflict": {
+                "psr/log": ">=3",
                 "symfony/dependency-injection": "<3.4",
                 "symfony/event-dispatcher": "<4.3|>=5",
                 "symfony/lock": "<4.4",
                 "symfony/process": "<3.3"
             },
             "provide": {
-                "psr/log-implementation": "1.0"
+                "psr/log-implementation": "1.0|2.0"
             },
             "require-dev": {
-                "psr/log": "~1.0",
+                "psr/log": "^1|^2",
                 "symfony/config": "^3.4|^4.0|^5.0",
                 "symfony/dependency-injection": "^3.4|^4.0|^5.0",
                 "symfony/event-dispatcher": "^4.3",
@@ -4819,7 +4846,7 @@
             "description": "Eases the creation of beautiful and testable command line interfaces",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/console/tree/v4.4.26"
+                "source": "https://github.com/symfony/console/tree/v4.4.30"
             },
             "funding": [
                 {
@@ -4835,25 +4862,26 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-06T09:12:27+00:00"
+            "time": "2021-08-25T19:27:26+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v4.4.26",
+            "version": "v4.4.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "a944d2f8e903dc99f5f1baf3eb74081352f0067f"
+                "reference": "75dd7094870feaa5be9ed2b6b1efcfc2b7d3b9b4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a944d2f8e903dc99f5f1baf3eb74081352f0067f",
-                "reference": "a944d2f8e903dc99f5f1baf3eb74081352f0067f",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/75dd7094870feaa5be9ed2b6b1efcfc2b7d3b9b4",
+                "reference": "75dd7094870feaa5be9ed2b6b1efcfc2b7d3b9b4",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
                 "psr/container": "^1.0",
+                "symfony/polyfill-php80": "^1.16",
                 "symfony/service-contracts": "^1.1.6|^2"
             },
             "conflict": {
@@ -4904,7 +4932,7 @@
             "description": "Allows you to standardize and centralize the way objects are constructed in your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/dependency-injection/tree/v4.4.26"
+                "source": "https://github.com/symfony/dependency-injection/tree/v4.4.31"
             },
             "funding": [
                 {
@@ -4920,25 +4948,26 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-24T08:08:16+00:00"
+            "time": "2021-09-21T06:20:06+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v4.4.25",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "047773e7016e4fd45102cedf4bd2558ae0d0c32f"
+                "reference": "2fe81680070043c4c80e7cedceb797e34f377bac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/047773e7016e4fd45102cedf4bd2558ae0d0c32f",
-                "reference": "047773e7016e4fd45102cedf4bd2558ae0d0c32f",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac",
+                "reference": "2fe81680070043c4c80e7cedceb797e34f377bac",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
-                "symfony/event-dispatcher-contracts": "^1.1"
+                "symfony/event-dispatcher-contracts": "^1.1",
+                "symfony/polyfill-php80": "^1.16"
             },
             "conflict": {
                 "symfony/dependency-injection": "<3.4"
@@ -4948,7 +4977,7 @@
                 "symfony/event-dispatcher-implementation": "1.1"
             },
             "require-dev": {
-                "psr/log": "~1.0",
+                "psr/log": "^1|^2|^3",
                 "symfony/config": "^3.4|^4.0|^5.0",
                 "symfony/dependency-injection": "^3.4|^4.0|^5.0",
                 "symfony/error-handler": "~3.4|~4.4",
@@ -4987,7 +5016,7 @@
             "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.25"
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30"
             },
             "funding": [
                 {
@@ -5003,7 +5032,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-26T17:39:37+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/event-dispatcher-contracts",
@@ -5086,21 +5115,22 @@
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.4.26",
+            "version": "v4.4.27",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "a501126eb6ec9288a3434af01b3d4499ec1268a0"
+                "reference": "517fb795794faf29086a77d99eb8f35e457837a7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/a501126eb6ec9288a3434af01b3d4499ec1268a0",
-                "reference": "a501126eb6ec9288a3434af01b3d4499ec1268a0",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/517fb795794faf29086a77d99eb8f35e457837a7",
+                "reference": "517fb795794faf29086a77d99eb8f35e457837a7",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
-                "symfony/polyfill-ctype": "~1.8"
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-php80": "^1.16"
             },
             "type": "library",
             "autoload": {
@@ -5128,7 +5158,7 @@
             "description": "Provides basic utilities for the filesystem",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/filesystem/tree/v4.4.26"
+                "source": "https://github.com/symfony/filesystem/tree/v4.4.27"
             },
             "funding": [
                 {
@@ -5144,24 +5174,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-30T07:12:23+00:00"
+            "time": "2021-07-21T12:19:41+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v4.4.25",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "ed33314396d968a8936c95f5bd1b88bd3b3e94a3"
+                "reference": "70362f1e112280d75b30087c7598b837c1b468b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/ed33314396d968a8936c95f5bd1b88bd3b3e94a3",
-                "reference": "ed33314396d968a8936c95f5bd1b88bd3b3e94a3",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/70362f1e112280d75b30087c7598b837c1b468b6",
+                "reference": "70362f1e112280d75b30087c7598b837c1b468b6",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3"
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
             },
             "type": "library",
             "autoload": {
@@ -5189,7 +5220,7 @@
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v4.4.25"
+                "source": "https://github.com/symfony/finder/tree/v4.4.30"
             },
             "funding": [
                 {
@@ -5205,24 +5236,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-26T11:20:16+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/options-resolver",
-            "version": "v4.4.25",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/options-resolver.git",
-                "reference": "2e607d627c70aa56284a02d34fc60dfe3a9a352e"
+                "reference": "fa0b12a3a47ed25749d47d6b4f61412fd5ca1554"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2e607d627c70aa56284a02d34fc60dfe3a9a352e",
-                "reference": "2e607d627c70aa56284a02d34fc60dfe3a9a352e",
+                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/fa0b12a3a47ed25749d47d6b4f61412fd5ca1554",
+                "reference": "fa0b12a3a47ed25749d47d6b4f61412fd5ca1554",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3"
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
             },
             "type": "library",
             "autoload": {
@@ -5255,7 +5287,7 @@
                 "options"
             ],
             "support": {
-                "source": "https://github.com/symfony/options-resolver/tree/v4.4.25"
+                "source": "https://github.com/symfony/options-resolver/tree/v4.4.30"
             },
             "funding": [
                 {
@@ -5271,7 +5303,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-26T11:20:16+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
@@ -5680,16 +5712,16 @@
         },
         {
             "name": "symfony/polyfill-php80",
-            "version": "v1.23.0",
+            "version": "v1.23.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0"
+                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/eca0bf41ed421bed1b57c4958bab16aa86b757d0",
-                "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
+                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
                 "shasum": ""
             },
             "require": {
@@ -5743,7 +5775,7 @@
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.0"
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
             },
             "funding": [
                 {
@@ -5759,7 +5791,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-02-19T12:13:01+00:00"
+            "time": "2021-07-28T13:41:28+00:00"
         },
         {
             "name": "symfony/polyfill-php81",
@@ -5842,20 +5874,21 @@
         },
         {
             "name": "symfony/process",
-            "version": "v4.4.26",
+            "version": "v4.4.30",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "7e812c84c3f2dba173d311de6e510edf701685a8"
+                "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/7e812c84c3f2dba173d311de6e510edf701685a8",
-                "reference": "7e812c84c3f2dba173d311de6e510edf701685a8",
+                "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
+                "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.1.3"
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
             },
             "type": "library",
             "autoload": {
@@ -5883,7 +5916,7 @@
             "description": "Executes commands in sub-processes",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/process/tree/v4.4.26"
+                "source": "https://github.com/symfony/process/tree/v4.4.30"
             },
             "funding": [
                 {
@@ -5899,7 +5932,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-09T14:57:04+00:00"
+            "time": "2021-08-04T20:31:23+00:00"
         },
         {
             "name": "symfony/service-contracts",
@@ -5982,16 +6015,16 @@
         },
         {
             "name": "symfony/stopwatch",
-            "version": "v4.4.25",
+            "version": "v4.4.27",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/stopwatch.git",
-                "reference": "80d9ae0c8a02bd291abf372764c0fc68cbd06c42"
+                "reference": "c85d997af06a58ba83e2d2538e335b894c24523d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/stopwatch/zipball/80d9ae0c8a02bd291abf372764c0fc68cbd06c42",
-                "reference": "80d9ae0c8a02bd291abf372764c0fc68cbd06c42",
+                "url": "https://api.github.com/repos/symfony/stopwatch/zipball/c85d997af06a58ba83e2d2538e335b894c24523d",
+                "reference": "c85d997af06a58ba83e2d2538e335b894c24523d",
                 "shasum": ""
             },
             "require": {
@@ -6024,7 +6057,7 @@
             "description": "Provides a way to profile code",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/stopwatch/tree/v4.4.25"
+                "source": "https://github.com/symfony/stopwatch/tree/v4.4.27"
             },
             "funding": [
                 {
@@ -6040,20 +6073,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-05-26T17:39:37+00:00"
+            "time": "2021-07-10T08:41:57+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v4.4.26",
+            "version": "v4.4.29",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "e096ef4b4c4c9a2f72c2ac660f54352cd31c60f8"
+                "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/e096ef4b4c4c9a2f72c2ac660f54352cd31c60f8",
-                "reference": "e096ef4b4c4c9a2f72c2ac660f54352cd31c60f8",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/3abcc4db06d4e776825eaa3ed8ad924d5bc7432a",
+                "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a",
                 "shasum": ""
             },
             "require": {
@@ -6095,7 +6128,7 @@
             "description": "Loads and dumps YAML files",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/yaml/tree/v4.4.26"
+                "source": "https://github.com/symfony/yaml/tree/v4.4.29"
             },
             "funding": [
                 {
@@ -6111,7 +6144,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-06-23T19:06:53+00:00"
+            "time": "2021-07-27T16:19:30+00:00"
         },
         {
             "name": "theseer/fdomdocument",
@@ -6159,16 +6192,16 @@
         },
         {
             "name": "theseer/tokenizer",
-            "version": "1.2.0",
+            "version": "1.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "75a63c33a8577608444246075ea0af0d052e452a"
+                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
-                "reference": "75a63c33a8577608444246075ea0af0d052e452a",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
+                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
                 "shasum": ""
             },
             "require": {
@@ -6197,7 +6230,7 @@
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
             "support": {
                 "issues": "https://github.com/theseer/tokenizer/issues",
-                "source": "https://github.com/theseer/tokenizer/tree/master"
+                "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
             },
             "funding": [
                 {
@@ -6205,20 +6238,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2020-07-12T23:59:07+00:00"
+            "time": "2021-07-28T10:34:58+00:00"
         },
         {
             "name": "vimeo/psalm",
-            "version": "4.8.1",
+            "version": "4.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vimeo/psalm.git",
-                "reference": "f73f2299dbc59a3e6c4d66cff4605176e728ee69"
+                "reference": "916b098b008f6de4543892b1e0651c1c3b92cbfa"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vimeo/psalm/zipball/f73f2299dbc59a3e6c4d66cff4605176e728ee69",
-                "reference": "f73f2299dbc59a3e6c4d66cff4605176e728ee69",
+                "url": "https://api.github.com/repos/vimeo/psalm/zipball/916b098b008f6de4543892b1e0651c1c3b92cbfa",
+                "reference": "916b098b008f6de4543892b1e0651c1c3b92cbfa",
                 "shasum": ""
             },
             "require": {
@@ -6228,6 +6261,7 @@
                 "composer/semver": "^1.4 || ^2.0 || ^3.0",
                 "composer/xdebug-handler": "^1.1 || ^2.0",
                 "dnoegel/php-xdg-base-dir": "^0.1.1",
+                "ext-ctype": "*",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
@@ -6237,7 +6271,7 @@
                 "felixfbecker/advanced-json-rpc": "^3.0.3",
                 "felixfbecker/language-server-protocol": "^1.5",
                 "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
-                "nikic/php-parser": "^4.10.5",
+                "nikic/php-parser": "^4.12",
                 "openlss/lib-array2xml": "^1.0",
                 "php": "^7.1|^8",
                 "sebastian/diff": "^3.0 || ^4.0",
@@ -6260,7 +6294,6 @@
                 "slevomat/coding-standard": "^7.0",
                 "squizlabs/php_codesniffer": "^3.5",
                 "symfony/process": "^4.3 || ^5.0",
-                "weirdan/phpunit-appveyor-reporter": "^1.0.0",
                 "weirdan/prophecy-shim": "^1.0 || ^2.0"
             },
             "suggest": {
@@ -6308,9 +6341,9 @@
             ],
             "support": {
                 "issues": "https://github.com/vimeo/psalm/issues",
-                "source": "https://github.com/vimeo/psalm/tree/4.8.1"
+                "source": "https://github.com/vimeo/psalm/tree/4.10.0"
             },
-            "time": "2021-06-20T23:03:20+00:00"
+            "time": "2021-09-04T21:00:09+00:00"
         },
         {
             "name": "webmozart/assert",
@@ -6429,7 +6462,8 @@
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": {
-        "php": "^7.2"
+        "php": "^7.2",
+        "ext-intl": "*"
     },
     "platform-dev": [],
     "platform-overrides": {
diff --git a/src/Query/Builder/QueryElementAttribute.php b/src/Query/Builder/QueryElementAttribute.php
index c1a3f9c..4f292b6 100644
--- a/src/Query/Builder/QueryElementAttribute.php
+++ b/src/Query/Builder/QueryElementAttribute.php
@@ -26,6 +26,7 @@
 use function is_bool;
 use function is_float;
 use function is_int;
+use NumberFormatter;
 use function sprintf;
 
 class QueryElementAttribute implements QueryElement, QueryElementDecorator
@@ -58,7 +59,8 @@ public function render(?callable $escaper = null): string
     {
         $attributes = [];
         if (is_float($this->weight)) {
-            $attributes[] = sprintf('$weight: %f;', $this->weight);
+            $formatter = new NumberFormatter('en_US', NumberFormatter::PATTERN_DECIMAL, '0.0###');
+            $attributes[] = sprintf('$weight: %s;', $formatter->format($this->weight));
         }
         if (is_int($this->slop)) {
             $attributes[] = sprintf('$slop: %d;', $this->slop);
diff --git a/tests/Query/Builder/QueryElementAttributeTest.php b/tests/Query/Builder/QueryElementAttributeTest.php
new file mode 100644
index 0000000..d3d86b5
--- /dev/null
+++ b/tests/Query/Builder/QueryElementAttributeTest.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query\Builder;
+
+use MacFJA\RediSearch\Query\Builder\QueryElementAttribute;
+use MacFJA\RediSearch\Query\Builder\RawElement;
+
+/**
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ *
+ * @uses \MacFJA\RediSearch\Query\Builder\RawElement
+ * @uses \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
+ *
+ * @internal
+ */
+class QueryElementAttributeTest extends \PHPUnit\Framework\TestCase
+{
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testOptions(string $expected, ?float $weight, ?int $slop, ?bool $inOrder, ?bool $phonetic): void
+    {
+        $baseElement = new RawElement('foo bar');
+        $attribute = new QueryElementAttribute($baseElement, $weight, $slop, $inOrder, $phonetic);
+        static::assertSame($expected, $attribute->render());
+    }
+
+    /**
+     * @return array<array<null|bool|float|int|string>>
+     */
+    public function dataProvider(): array
+    {
+        return [
+            ['foo bar', null, null, null, null],
+            ['(foo bar) => { $weight: 0.5; }', 0.5, null, null, null],
+            ['(foo bar) => { $weight: 2.0; }', 2.0, null, null, null],
+            ['(foo bar) => { $slop: 2; }', null, 2, null, null],
+            ['(foo bar) => { $inorder: true; }', null, null, true, null],
+            ['(foo bar) => { $inorder: false; }', null, null, false, null],
+            ['(foo bar) => { $phonetic: true; }', null, null, null, true],
+            ['(foo bar) => { $phonetic: false; }', null, null, null, false],
+            ['(foo bar) => { $weight: 1.3; $slop: 4; $inorder: true; $phonetic: false; }', 1.3, 4, true, false],
+        ];
+    }
+}
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index 6523b8e..9ec9ac5 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -243,8 +243,8 @@ public function testDocAttributeExample1(): void
         ;
 
         static::assertSame($expected, str_replace(
-            ['00000; ', ' })', '(('],
-            ['; ', ' }', '('],
+            [' })', '(('],
+            [' }', '('],
             $builder->render()
         ));
     }
@@ -267,8 +267,8 @@ public function testDocAttributeExample2(): void
         ;
 
         static::assertSame($expected, str_replace(
-            ['00000; ', ' })', '(~('],
-            ['; ', ' }', '~('],
+            [' })', '(~('],
+            [' }', '~('],
             $builder->render()
         ));
     }
@@ -293,8 +293,8 @@ public function testDocAttributeExample2bis(): void
         ;
 
         static::assertNotSame($expected, str_replace(
-            ['00000; ', ' })', '(~('],
-            ['; ', ' }', '~('],
+            [' })', '(~('],
+            [' }', '~('],
             $builder->render()
         ));
     }

From b3d9e066e760c4502f0b2b30d154f70b2ca0f132 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 14:44:27 +0200
Subject: [PATCH 03/60] Add documentation test for query prefix

---
 src/Query/Builder/Prefix.php |  9 ++-------
 tests/Query/BuilderTest.php  | 16 ++++++++++++++++
 2 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/src/Query/Builder/Prefix.php b/src/Query/Builder/Prefix.php
index 6648dc1..f8e5bef 100644
--- a/src/Query/Builder/Prefix.php
+++ b/src/Query/Builder/Prefix.php
@@ -21,8 +21,7 @@
 
 namespace MacFJA\RediSearch\Query\Builder;
 
-use function call_user_func;
-use function is_callable;
+use MacFJA\RediSearch\Query\Escaper;
 
 class Prefix implements QueryElement
 {
@@ -36,11 +35,7 @@ public function __construct(string $prefix)
 
     public function render(?callable $escaper = null): string
     {
-        if (is_callable($escaper)) {
-            return call_user_func($escaper, $this->prefix).'*';
-        }
-
-        return $this->prefix.'*';
+        return Escaper::escapeWord($this->prefix).'*';
     }
 
     public function priority(): int
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index 9ec9ac5..039088f 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -192,6 +192,22 @@ public function testDocGeoExample1(): void
         ));
     }
 
+    /**
+     * @see https://oss.redis.com/redisearch/Query_Syntax/#prefix_matching
+     */
+    public function testDocPrefixExample1(): void
+    {
+        $expected = 'hel* world';
+        $builder = new Builder();
+
+        $builder
+            ->addElement(new Builder\Prefix('hel'))
+            ->addString('world')
+        ;
+
+        static::assertSame($expected, $builder->render());
+    }
+
     /**
      * @see https://oss.redislabs.com/redisearch/Query_Syntax/#fuzzy_matching
      */

From 9c48cc58e7ad8e8d5e6ea940c38516d44a46a918 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 14:48:10 +0200
Subject: [PATCH 04/60] Add test on completeness of fields and terms in facets
 (+ UT)

---
 src/Query/Builder/FieldFacet.php            |   5 +
 src/Query/Builder/GeoFacet.php              |   5 +
 src/Query/Builder/NumericFacet.php          |   5 +
 src/Query/Builder/TagFacet.php              |  11 ++
 src/Query/Builder/TextFacet.php             |   9 ++
 tests/Query/Builder/IncompleteFacetTest.php | 106 ++++++++++++++++++++
 6 files changed, 141 insertions(+)
 create mode 100644 tests/Query/Builder/IncompleteFacetTest.php

diff --git a/src/Query/Builder/FieldFacet.php b/src/Query/Builder/FieldFacet.php
index 2a6b09c..2df1e19 100644
--- a/src/Query/Builder/FieldFacet.php
+++ b/src/Query/Builder/FieldFacet.php
@@ -21,6 +21,8 @@
 
 namespace MacFJA\RediSearch\Query\Builder;
 
+use function count;
+use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Query\Escaper;
 
 class FieldFacet implements QueryElement
@@ -35,6 +37,9 @@ class FieldFacet implements QueryElement
      */
     public function __construct(array $fields, QueryElement $element)
     {
+        if (0 === count($fields)) {
+            throw new NotEnoughFieldsException();
+        }
         $this->fields = array_map([Escaper::class, 'escapeFieldName'], $fields);
         $this->element = $element;
     }
diff --git a/src/Query/Builder/GeoFacet.php b/src/Query/Builder/GeoFacet.php
index 58e39b4..6d404fa 100644
--- a/src/Query/Builder/GeoFacet.php
+++ b/src/Query/Builder/GeoFacet.php
@@ -21,6 +21,8 @@
 
 namespace MacFJA\RediSearch\Query\Builder;
 
+use function count;
+use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Exception\UnknownUnitException;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
 use Respect\Validation\Rules\In;
@@ -32,6 +34,9 @@ class GeoFacet extends FieldFacet
      */
     public function __construct(array $fields, float $lon, float $lat, float $radius, string $unit)
     {
+        if (0 === count($fields)) {
+            throw new NotEnoughFieldsException();
+        }
         if (!(new In(GeoFilterOption::UNITS))->validate($unit)) {
             throw new UnknownUnitException($unit);
         }
diff --git a/src/Query/Builder/NumericFacet.php b/src/Query/Builder/NumericFacet.php
index 50f2302..87c9255 100644
--- a/src/Query/Builder/NumericFacet.php
+++ b/src/Query/Builder/NumericFacet.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function is_array;
+use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\NumericRangeTrait;
 
 class NumericFacet extends FieldFacet
@@ -33,9 +34,13 @@ class NumericFacet extends FieldFacet
      */
     public function __construct($fields, ?float $min, ?float $max, bool $isMinInclusive = true, bool $isMaxInclusive = true)
     {
+        if (empty($fields)) {
+            throw new NotEnoughFieldsException();
+        }
         if (!is_array($fields)) {
             $fields = [$fields];
         }
+
         $bounds = self::renderBound($min, $max, $isMinInclusive, $isMaxInclusive);
         $group = new EncapsulationGroup('[', ']', AndGroup::fromStrings(...$bounds));
         parent::__construct($fields, $group);
diff --git a/src/Query/Builder/TagFacet.php b/src/Query/Builder/TagFacet.php
index a9cac96..96a0920 100644
--- a/src/Query/Builder/TagFacet.php
+++ b/src/Query/Builder/TagFacet.php
@@ -21,10 +21,21 @@
 
 namespace MacFJA\RediSearch\Query\Builder;
 
+use function count;
+use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
+use MacFJA\RediSearch\Exception\NotEnoughTermsException;
+
 class TagFacet extends FieldFacet
 {
     public function __construct(array $fields, string ...$values)
     {
+        if (0 === count($fields)) {
+            throw new NotEnoughFieldsException();
+        }
+        if (0 === count($values)) {
+            throw new NotEnoughTermsException();
+        }
+
         $orGroup = new OrGroup(array_map(static function (string $value): QueryElement {
             return new Word($value);
         }, $values));
diff --git a/src/Query/Builder/TextFacet.php b/src/Query/Builder/TextFacet.php
index 144d89c..61750a5 100644
--- a/src/Query/Builder/TextFacet.php
+++ b/src/Query/Builder/TextFacet.php
@@ -22,6 +22,8 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function count;
+use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
+use MacFJA\RediSearch\Exception\NotEnoughTermsException;
 
 class TextFacet extends FieldFacet
 {
@@ -30,6 +32,13 @@ class TextFacet extends FieldFacet
      */
     public function __construct(array $fields, string ...$values)
     {
+        if (0 === count($fields)) {
+            throw new NotEnoughFieldsException();
+        }
+        if (0 === count($values)) {
+            throw new NotEnoughTermsException();
+        }
+
         $orGroup = new OrGroup(array_map(static function (string $value): QueryElement {
             return new ExactMatch($value);
         }, $values));
diff --git a/tests/Query/Builder/IncompleteFacetTest.php b/tests/Query/Builder/IncompleteFacetTest.php
new file mode 100644
index 0000000..a3a530a
--- /dev/null
+++ b/tests/Query/Builder/IncompleteFacetTest.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query\Builder;
+
+use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
+use MacFJA\RediSearch\Exception\NotEnoughTermsException;
+use MacFJA\RediSearch\Query\Builder\FieldFacet;
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use MacFJA\RediSearch\Query\Builder\NumericFacet;
+use MacFJA\RediSearch\Query\Builder\RawElement;
+use MacFJA\RediSearch\Query\Builder\TagFacet;
+use MacFJA\RediSearch\Query\Builder\TextFacet;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Exception\NotEnoughFieldsException
+ * @covers \MacFJA\RediSearch\Exception\NotEnoughTermsException
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
+ *
+ * @uses  \MacFJA\RediSearch\Query\Builder\RawElement
+ *
+ * @internal
+ */
+class IncompleteFacetTest extends TestCase
+{
+    public function testTagFacetMissingTerms(): void
+    {
+        $this->expectException(NotEnoughTermsException::class);
+        $this->expectExceptionMessage('You must have at least one term');
+
+        new TagFacet(['text']);
+    }
+
+    public function testTagFacetMissingFields(): void
+    {
+        $this->expectException(NotEnoughFieldsException::class);
+        $this->expectExceptionMessage('You must have at least one field');
+
+        new TagFacet([], 'foo');
+    }
+
+    public function testTextFacetMissingTerms(): void
+    {
+        $this->expectException(NotEnoughTermsException::class);
+        $this->expectExceptionMessage('You must have at least one term');
+
+        new TextFacet(['text']);
+    }
+
+    public function testTextFacetMissingFields(): void
+    {
+        $this->expectException(NotEnoughFieldsException::class);
+        $this->expectExceptionMessage('You must have at least one field');
+
+        new TextFacet([], 'foo');
+    }
+
+    public function testNumericFacetMissingFields(): void
+    {
+        $this->expectException(NotEnoughFieldsException::class);
+        $this->expectExceptionMessage('You must have at least one field');
+
+        new NumericFacet([], 0, null);
+    }
+
+    public function testGeoFacetMissingFields(): void
+    {
+        $this->expectException(NotEnoughFieldsException::class);
+        $this->expectExceptionMessage('You must have at least one field');
+
+        new GeoFacet([], 0, 0, 5, GeoFilterOption::UNIT_METERS);
+    }
+
+    public function testFieldFacetMissingFields(): void
+    {
+        $this->expectException(NotEnoughFieldsException::class);
+        $this->expectExceptionMessage('You must have at least one field');
+
+        new FieldFacet([], new RawElement(''));
+    }
+}

From 84cf02fd1a9081d0bd32616eb557edfe817eaa56 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 15:07:46 +0200
Subject: [PATCH 05/60] Add UT for Escaper

---
 tests/Query/EscaperTest.php             | 144 ++++++++++++++++++++++++
 tests/fixtures/Query/EscaperFixture.csv |  32 ++++++
 2 files changed, 176 insertions(+)
 create mode 100644 tests/Query/EscaperTest.php
 create mode 100644 tests/fixtures/Query/EscaperFixture.csv

diff --git a/tests/Query/EscaperTest.php b/tests/Query/EscaperTest.php
new file mode 100644
index 0000000..40f5ce7
--- /dev/null
+++ b/tests/Query/EscaperTest.php
@@ -0,0 +1,144 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query;
+
+use function count;
+use MacFJA\RediSearch\Query\Escaper;
+
+/**
+ * @covers \MacFJA\RediSearch\Query\Escaper
+ *
+ * @internal
+ */
+class EscaperTest extends \PHPUnit\Framework\TestCase
+{
+    private const TEST_TO_FIXTURE = [
+        'testEscapeFieldName' => 'field name',
+        'testEscapeWord' => 'word',
+        'testEscapeExactMatch' => 'exact match',
+        'testEscapeFuzzy' => 'fuzzy',
+        'testEscapeNegation' => 'negation',
+        'testEscapeOptional' => 'optional',
+        'testEscapeNothing' => 'input',
+    ];
+    /**
+     * @var array<array<string>>
+     */
+    private $dataset = [];
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeNothing(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeNothing($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeFieldName(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeFieldName($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeWord(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeWord($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeExactMatch(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeExactMatch($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeFuzzy(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeFuzzy($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeNegation(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeNegation($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testEscapeOptional(string $input, string $escaped): void
+    {
+        static::assertSame($escaped, Escaper::escapeOptional($input));
+    }
+
+    /**
+     * @return array<array<string>>
+     */
+    public function dataProvider(string $testName): array
+    {
+        $this->buildDataset();
+
+        return array_map(static function ($row) use ($testName) {
+            return [$row['input'], $row[self::TEST_TO_FIXTURE[$testName]]];
+        }, $this->dataset);
+    }
+
+    private function buildDataset(): void
+    {
+        if (count($this->dataset) > 0) {
+            return;
+        }
+
+        $datasetPath = __DIR__.'/../fixtures/Query/EscaperFixture.csv';
+
+        if (!file_exists($datasetPath)) {
+            static::markTestSkipped('Unable to find fixture data');
+        }
+        $handle = fopen($datasetPath, 'rb');
+
+        if (false === $handle) {
+            static::markTestSkipped('Unable to load fixture data');
+        }
+
+        $header = fgetcsv($handle) ?: [];
+        fgetcsv($handle); // Description
+        while (($row = fgetcsv($handle, 0, ',', '"', '"')) !== false) {
+            $this->dataset[] = array_combine($header, $row ?: []) ?: [];
+        }
+        fclose($handle);
+
+        if (0 === count($this->dataset)) {
+            static::markTestSkipped('Unable to load fixture data');
+        }
+    }
+}
diff --git a/tests/fixtures/Query/EscaperFixture.csv b/tests/fixtures/Query/EscaperFixture.csv
new file mode 100644
index 0000000..cf2aedb
--- /dev/null
+++ b/tests/fixtures/Query/EscaperFixture.csv
@@ -0,0 +1,32 @@
+"input","field name","fuzzy","exact match","optional","negation","word"
+"","'@' + tranformed + ':'","'%' + tranformed + '%'","'""' + tranformed + '""'","'~' + transformed","'-' + transformed","transformed"
+"foobar","foobar","foobar","foobar","foobar","foobar","foobar"
+"""foobar","\""foobar","\""foobar","\""foobar","\""foobar","\""foobar","\""foobar"
+"foobar""","foobar\""","foobar\""","foobar\""","foobar\""","foobar\""","foobar\"""
+"""foobar""","\""foobar\""","\""foobar\""","\""foobar\""","\""foobar\""","\""foobar\""","\""foobar\"""
+"@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar"
+"foobar:","foobar\:","foobar\:","foobar\:","foobar\:","foobar\:","foobar\:"
+"@foobar:","\@foobar\:","\@foobar\:","\@foobar\:","\@foobar\:","\@foobar\:","\@foobar\:"
+"-foobar","\-foobar","\-foobar","\-foobar","\-foobar","\-foobar","\-foobar"
+"foo@bar","foo\@bar","foo\@bar","foo\@bar","foo\@bar","foo\@bar","foo\@bar"
+"foo-bar","foo\-bar","foo\-bar","foo\-bar","foo\-bar","foo\-bar","foo\-bar"
+"foo.bar","foo\.bar","foo\.bar","foo\.bar","foo\.bar","foo\.bar","foo\.bar"
+"foobar-","foobar\-","foobar\-","foobar\-","foobar\-","foobar\-","foobar\-"
+"foo,bar","foo\,bar","foo\,bar","foo\,bar","foo\,bar","foo\,bar","foo\,bar"
+"\@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar","\@foobar"
+"foo""bar","foo\""bar","foo\""bar","foo\""bar","foo\""bar","foo\""bar","foo\""bar"
+"%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar"
+"foo%bar","foo\%bar","foo\%bar","foo\%bar","foo\%bar","foo\%bar","foo\%bar"
+"%foobar%","\%foobar\%","\%foobar\%","\%foobar\%","\%foobar\%","\%foobar\%","\%foobar\%"
+"\%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar","\%foobar"
+"\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar","\\\@foobar"
+"foo=bar","foo\=bar","foo\=bar","foo\=bar","foo\=bar","foo\=bar","foo\=bar"
+"~foobar","\~foobar","\~foobar","\~foobar","\~foobar","\~foobar","\~foobar"
+"foobar*","foobar\*","foobar\*","foobar\*","foobar\*","foobar\*","foobar\*"
+"foo|bar","foo\|bar","foo\|bar","foo\|bar","foo\|bar","foo\|bar","foo\|bar"
+"foo(bar","foo\(bar","foo\(bar","foo\(bar","foo\(bar","foo\(bar","foo\(bar"
+"foo{bar","foo\{bar","foo\{bar","foo\{bar","foo\{bar","foo\{bar","foo\{bar"
+"foo[bar","foo\[bar","foo\[bar","foo\[bar","foo\[bar","foo\[bar","foo\[bar"
+"foo$bar","foo\$bar","foo\$bar","foo\$bar","foo\$bar","foo\$bar","foo\$bar"
+"-29","\-29","-29","-29","-29","\-29","-29"
+"29","29","29","29","29","\29","29"
\ No newline at end of file

From c6a58bd246623c7e080fd316fb7229ef1055d010 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 15:15:41 +0200
Subject: [PATCH 06/60] Add RediSearch 2.0.11 + 2.0.12 options + fix version
 checking

---
 src/Redis/Command/AbstractCommand.php         |  6 +-
 src/Redis/Command/AddFieldOptionTrait.php     |  3 +-
 .../BaseCreateFieldOptionTrait.php            | 56 +++++++++++++++++++
 .../Command/CreateCommand/GeoFieldOption.php  | 13 ++---
 .../CreateCommand/NumericFieldOption.php      | 13 ++---
 .../Command/CreateCommand/TagFieldOption.php  | 15 +++--
 .../Command/CreateCommand/TextFieldOption.php | 13 ++---
 .../Command/SearchCommand/SummarizeOption.php |  2 +-
 .../Redis/Command/AddFieldOptionTraitTest.php |  2 +
 tests/Redis/Command/AlterTest.php             |  1 +
 tests/Redis/Command/CreateTest.php            | 36 ++++++++++--
 tests/Redis/Command/SearchTest.php            |  3 +-
 12 files changed, 120 insertions(+), 43 deletions(-)
 create mode 100644 src/Redis/Command/CreateCommand/BaseCreateFieldOptionTrait.php

diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index 0b7c3de..c28d5da 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -32,6 +32,8 @@
  */
 abstract class AbstractCommand extends Command
 {
+    public const MAX_IMPLEMENTED_VERSION = '2.0.12';
+    public const MIN_IMPLEMENTED_VERSION = '2.0.0';
     /**
      * @var array<array<CommandOption>|CommandOption|mixed>
      */
@@ -153,8 +155,8 @@ private function buildArguments(): void
             return $option->isCompatible($this->rediSearchVersion) && $option->isValid();
         });
 
-        $arguments = array_reduce($arguments, static function ($carry, CommandOption $option) {
-            return array_merge($carry, $option->render());
+        $arguments = array_reduce($arguments, function ($carry, CommandOption $option) {
+            return array_merge($carry, $option->render($this->rediSearchVersion));
         }, []);
         $this->setRawArguments($arguments);
     }
diff --git a/src/Redis/Command/AddFieldOptionTrait.php b/src/Redis/Command/AddFieldOptionTrait.php
index b2401f4..389d6ec 100644
--- a/src/Redis/Command/AddFieldOptionTrait.php
+++ b/src/Redis/Command/AddFieldOptionTrait.php
@@ -63,12 +63,13 @@ public function addGeoField(string $name, bool $noIndex = false): self
         );
     }
 
-    public function addTagField(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false): self
+    public function addTagField(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false, bool $caseSensitive = false): self
     {
         return $this->addField(
             (new TagFieldOption())
                 ->setField($name)
                 ->setSeparator($separator)
+                ->setCaseSensitive($caseSensitive)
                 ->setSortable($sortable)
                 ->setNoIndex($noIndex)
         );
diff --git a/src/Redis/Command/CreateCommand/BaseCreateFieldOptionTrait.php b/src/Redis/Command/CreateCommand/BaseCreateFieldOptionTrait.php
new file mode 100644
index 0000000..916be34
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/BaseCreateFieldOptionTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use Respect\Validation\Rules\Callback;
+
+trait BaseCreateFieldOptionTrait
+{
+    /**
+     * @param array<array<CommandOption>|CommandOption|mixed> $options $options
+     *
+     * @return array<array<CommandOption>|CommandOption|mixed> $options
+     */
+    private function getConstructorOptions(string $type, array $options = []): array
+    {
+        $self = $this;
+
+        return array_merge(
+            [
+                'field' => new NamelessOption(null, '>=2.0.0'),
+                'type' => new FlagOption($type, true, '>=2.0.0'),
+            ],
+            $options,
+            [
+                'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
+                'un_normalized_sortable' => new CustomValidatorOption(new FlagOption('UNF', false, '>=2.0.12'), new Callback(static function () use ($self) {
+                    return $self->getDataOfOption('sortable');
+                })),
+                'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
+            ]
+        );
+    }
+}
diff --git a/src/Redis/Command/CreateCommand/GeoFieldOption.php b/src/Redis/Command/CreateCommand/GeoFieldOption.php
index 073b8cc..e2a50ef 100644
--- a/src/Redis/Command/CreateCommand/GeoFieldOption.php
+++ b/src/Redis/Command/CreateCommand/GeoFieldOption.php
@@ -21,28 +21,23 @@
 
 namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
 
-use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
-use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
 
 /**
  * @method GeoFieldOption setField(string $name)
  * @method GeoFieldOption setNoIndex(bool $active)
  * @method GeoFieldOption setSortable(bool $active)
+ * @method GeoFieldOption setUnNormalizedSortable(bool $unNormalized)
  */
 class GeoFieldOption extends GroupedOption implements CreateCommandFieldOption
 {
+    use BaseCreateFieldOptionTrait;
     use WithPublicGroupedSetterTrait;
 
     public function __construct()
     {
-        parent::__construct([
-            'field' => new NamelessOption(null, '>=2.0.0'),
-            'type' => new FlagOption('GEO', true, '>=2.0.0'),
-            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
-            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.10'),
-        ], ['field', 'type'], ['type'], '>=2.0.0');
+        parent::__construct($this->getConstructorOptions('GEO'), ['field', 'type'], ['type'], '>=2.0.0');
     }
 
     public function getFieldName(): string
@@ -55,6 +50,6 @@ public function getFieldName(): string
      */
     protected function publicSetter(): array
     {
-        return ['field', 'no_index', 'sortable'];
+        return ['field', 'no_index', 'sortable', 'un_normalized_sortable'];
     }
 }
diff --git a/src/Redis/Command/CreateCommand/NumericFieldOption.php b/src/Redis/Command/CreateCommand/NumericFieldOption.php
index d14c86a..7f957cb 100644
--- a/src/Redis/Command/CreateCommand/NumericFieldOption.php
+++ b/src/Redis/Command/CreateCommand/NumericFieldOption.php
@@ -21,28 +21,23 @@
 
 namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
 
-use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
-use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
 
 /**
  * @method NumericFieldOption setField(string $name)
  * @method NumericFieldOption setNoIndex(bool $active)
  * @method NumericFieldOption setSortable(bool $active)
+ * @method NumericFieldOption setUnNormalizedSortable(bool $unNormalized)
  */
 class NumericFieldOption extends GroupedOption implements CreateCommandFieldOption
 {
+    use BaseCreateFieldOptionTrait;
     use WithPublicGroupedSetterTrait;
 
     public function __construct()
     {
-        parent::__construct([
-            'field' => new NamelessOption(null, '>=2.0.0'),
-            'type' => new FlagOption('NUMERIC', true, '>=2.0.0'),
-            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
-            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
-        ], ['field', 'type'], ['type'], '>=2.0.0');
+        parent::__construct($this->getConstructorOptions('NUMERIC'), ['field', 'type'], ['type'], '>=2.0.0');
     }
 
     public function getFieldName(): string
@@ -55,6 +50,6 @@ public function getFieldName(): string
      */
     protected function publicSetter(): array
     {
-        return ['field', 'no_index', 'sortable'];
+        return ['field', 'no_index', 'sortable', 'un_normalized_sortable'];
     }
 }
diff --git a/src/Redis/Command/CreateCommand/TagFieldOption.php b/src/Redis/Command/CreateCommand/TagFieldOption.php
index a77d9d8..cee3390 100644
--- a/src/Redis/Command/CreateCommand/TagFieldOption.php
+++ b/src/Redis/Command/CreateCommand/TagFieldOption.php
@@ -25,7 +25,6 @@
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
-use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
 use Respect\Validation\Rules\Length;
 
@@ -34,20 +33,20 @@
  * @method TagFieldOption setNoIndex(bool $active)
  * @method TagFieldOption setSortable(bool $active)
  * @method TagFieldOption setSeparator(?string $separator)
+ * @method TagFieldOption setCaseSensitive(bool $active)
+ * @method TagFieldOption setUnNormalizedSortable(bool $unNormalized)
  */
 class TagFieldOption extends GroupedOption implements CreateCommandFieldOption
 {
+    use BaseCreateFieldOptionTrait;
     use WithPublicGroupedSetterTrait;
 
     public function __construct()
     {
-        parent::__construct([
-            'field' => new NamelessOption(null, '>=2.0.0'),
-            'type' => new FlagOption('TAG', true, '>=2.0.0'),
+        parent::__construct($this->getConstructorOptions('TAG', [
             'separator' => new CustomValidatorOption(new NamedOption('SEPARATOR', null, '>=2.0.0'), new Length(1, 1)),
-            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
-            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
-        ], ['field', 'type'], ['type'], '>=2.0.0');
+            'case_sensitive' => new FlagOption('CASESENSITIVE', false, '>=2.0.11'),
+        ]), ['field', 'type'], ['type'], '>=2.0.0');
     }
 
     public function getFieldName(): string
@@ -60,6 +59,6 @@ public function getFieldName(): string
      */
     protected function publicSetter(): array
     {
-        return ['field', 'separator', 'sortable', 'no_index'];
+        return ['field', 'separator', 'sortable', 'no_index', 'case_sensitive', 'un_normalized_sortable'];
     }
 }
diff --git a/src/Redis/Command/CreateCommand/TextFieldOption.php b/src/Redis/Command/CreateCommand/TextFieldOption.php
index de6eb1f..5efa94b 100644
--- a/src/Redis/Command/CreateCommand/TextFieldOption.php
+++ b/src/Redis/Command/CreateCommand/TextFieldOption.php
@@ -25,7 +25,6 @@
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
-use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
 
 /**
@@ -35,22 +34,20 @@
  * @method TextFieldOption setNoStem(bool $active)
  * @method TextFieldOption setWeight(?float $weight)
  * @method TextFieldOption setPhonetic(?string $phonetic)
+ * @method TextFieldOption setUnNormalizedSortable(bool $unNormalized)
  */
 class TextFieldOption extends GroupedOption implements CreateCommandFieldOption
 {
+    use BaseCreateFieldOptionTrait;
     use WithPublicGroupedSetterTrait;
 
     public function __construct()
     {
-        parent::__construct([
-            'field' => new NamelessOption(null, '>=2.0.0'),
-            'type' => new FlagOption('TEXT', true, '>=2.0.0'),
+        parent::__construct($this->getConstructorOptions('TEXT', [
             'no_stem' => new FlagOption('NOSTEM', false, '>=2.0.0'),
             'weight' => CustomValidatorOption::isNumeric(new NamedOption('WEIGHT', null, '>=2.0.0')),
             'phonetic' => CustomValidatorOption::allowedValues(new NamedOption('PHONETIC', null, '>=2.0.0'), ['dm:en', 'dm:fr', 'dm:pt', 'dm:es']),
-            'sortable' => new FlagOption('SORTABLE', false, '>=2.0.0'),
-            'no_index' => new FlagOption('NOINDEX', false, '>=2.0.0'),
-        ], ['field', 'type'], ['type'], '>=2.0.0');
+        ]), ['field', 'type'], ['type'], '>=2.0.0');
     }
 
     public function getFieldName(): string
@@ -63,6 +60,6 @@ public function getFieldName(): string
      */
     protected function publicSetter(): array
     {
-        return ['field', 'no_stem', 'weight', 'phonetic', 'sortable', 'no_index'];
+        return ['field', 'no_stem', 'weight', 'phonetic', 'sortable', 'no_index', 'un_normalized_sortable'];
     }
 }
diff --git a/src/Redis/Command/SearchCommand/SummarizeOption.php b/src/Redis/Command/SearchCommand/SummarizeOption.php
index b768678..6ebebba 100644
--- a/src/Redis/Command/SearchCommand/SummarizeOption.php
+++ b/src/Redis/Command/SearchCommand/SummarizeOption.php
@@ -37,7 +37,7 @@ public function __construct()
             'fields' => new NotEmptyOption(new NumberedOption('FIELDS', null, '>=2.0.0')),
             'frags' => CV::isNumeric(new NamedOption('FRAGS', null, '>=2.0.0')),
             'len' => CV::isNumeric(new NamedOption('LEN', null, '>=2.0.0')),
-            'separator' => new NamedOption('SEPARATOR', null, '>=2.0.à'),
+            'separator' => new NamedOption('SEPARATOR', null, '>=2.0.0'),
         ], ['type'], ['type'], '>=2.0.0');
     }
 }
diff --git a/tests/Redis/Command/AddFieldOptionTraitTest.php b/tests/Redis/Command/AddFieldOptionTraitTest.php
index 1f09c57..0b5ade7 100644
--- a/tests/Redis/Command/AddFieldOptionTraitTest.php
+++ b/tests/Redis/Command/AddFieldOptionTraitTest.php
@@ -31,6 +31,8 @@
  * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
  * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
diff --git a/tests/Redis/Command/AlterTest.php b/tests/Redis/Command/AlterTest.php
index 02af61e..4ab859b 100644
--- a/tests/Redis/Command/AlterTest.php
+++ b/tests/Redis/Command/AlterTest.php
@@ -36,6 +36,7 @@
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
  *
  * @internal
  */
diff --git a/tests/Redis/Command/CreateTest.php b/tests/Redis/Command/CreateTest.php
index 2df8579..00c028d 100644
--- a/tests/Redis/Command/CreateTest.php
+++ b/tests/Redis/Command/CreateTest.php
@@ -21,7 +21,9 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\Create;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -80,7 +82,7 @@ public function testGetId(): void
 
     public function testFullOption(): void
     {
-        $command = new Create();
+        $command = new Create(AbstractCommand::MAX_IMPLEMENTED_VERSION);
         $command
             ->addNumericField('num1')
             ->addNumericField('num2', false, true)
@@ -90,7 +92,7 @@ public function testFullOption(): void
             ->addGeoField('geo2', true)
             ->addTextField('text1')
             ->addTextField('text2', true)
-            ->addTextField('text3', false, 1.0, )
+            ->addTextField('text3', false, 1.0)
             ->addTextField('text4', false, null, 'dm:en')
             ->addTextField('text5', false, null, null, true)
             ->addTextField('text6', false, null, null, false, true)
@@ -99,7 +101,8 @@ public function testFullOption(): void
             ->addTagField('tag2', '|')
             ->addTagField('tag3', null, true)
             ->addTagField('tag4', null, false, true)
-            ->addTagField('tag5', '#', true, true)
+            ->addTagField('tag5', null, false, false, true)
+            ->addTagField('tag6', '#', true, true, true)
             ->setPrefixes('doc_', 'document_')
             ->setIndex('idx')
             ->setDefaultLanguage('french')
@@ -149,7 +152,8 @@ public function testFullOption(): void
             'tag2', 'TAG', 'SEPARATOR', '|',
             'tag3', 'TAG', 'SORTABLE',
             'tag4', 'TAG', 'NOINDEX',
-            'tag5', 'TAG', 'SEPARATOR', '#', 'SORTABLE', 'NOINDEX',
+            'tag5', 'TAG', 'CASESENSITIVE',
+            'tag6', 'TAG', 'SEPARATOR', '#', 'CASESENSITIVE', 'SORTABLE', 'NOINDEX',
         ], $command->getArguments());
     }
 
@@ -163,4 +167,28 @@ public function testNoStopWords(): void
 
         static::assertSame(['idx', 'STOPWORDS', 0, 'SCHEMA', 'foo', 'TEXT'], $command->getArguments());
     }
+
+    public function testUnNormalizedForm(): void
+    {
+        $command = new Create('2.0.0');
+        $command->setIndex('idx');
+
+        $textField = new TextFieldOption();
+        $textField->setField('foo')
+            ->setSortable(true)
+            ->setUnNormalizedSortable(true)
+        ;
+
+        $command->addField($textField);
+
+        static::assertSame(['idx', 'SCHEMA', 'foo', 'TEXT', 'SORTABLE'], $command->getArguments());
+
+        // Change version
+        $command->setRediSearchVersion('2.0.12');
+        static::assertSame(['idx', 'SCHEMA', 'foo', 'TEXT', 'SORTABLE', 'UNF'], $command->getArguments());
+
+        // Remove sorting
+        $textField->setSortable(false);
+        static::assertSame(['idx', 'SCHEMA', 'foo', 'TEXT'], $command->getArguments());
+    }
 }
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index 824dd72..47a50d3 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\Search;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
 use PHPUnit\Framework\TestCase;
@@ -71,7 +72,7 @@ public function testSizeOptions(): void
 
     public function testFullOption(): void
     {
-        $command = new Search();
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')

From ebe41444bfb9427afca2eb512b2aa7c0b0f2cfc2 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 15:16:03 +0200
Subject: [PATCH 07/60] Add more UT

---
 tests/Query/Builder/IncludeSpaceTest.php | 139 +++++++++++++++++++++++
 tests/Query/Builder/PriorityTest.php     | 122 ++++++++++++++++++++
 2 files changed, 261 insertions(+)
 create mode 100644 tests/Query/Builder/IncludeSpaceTest.php
 create mode 100644 tests/Query/Builder/PriorityTest.php

diff --git a/tests/Query/Builder/IncludeSpaceTest.php b/tests/Query/Builder/IncludeSpaceTest.php
new file mode 100644
index 0000000..4dc02de
--- /dev/null
+++ b/tests/Query/Builder/IncludeSpaceTest.php
@@ -0,0 +1,139 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query\Builder;
+
+use MacFJA\RediSearch\Query\Builder;
+use MacFJA\RediSearch\Query\Builder\AndGroup;
+use MacFJA\RediSearch\Query\Builder\EncapsulationGroup;
+use MacFJA\RediSearch\Query\Builder\ExactMatch;
+use MacFJA\RediSearch\Query\Builder\FieldFacet;
+use MacFJA\RediSearch\Query\Builder\Fuzzy;
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use MacFJA\RediSearch\Query\Builder\Negation;
+use MacFJA\RediSearch\Query\Builder\NumericFacet;
+use MacFJA\RediSearch\Query\Builder\Optional;
+use MacFJA\RediSearch\Query\Builder\OrGroup;
+use MacFJA\RediSearch\Query\Builder\Prefix;
+use MacFJA\RediSearch\Query\Builder\QueryElementAttribute;
+use MacFJA\RediSearch\Query\Builder\RawElement;
+use MacFJA\RediSearch\Query\Builder\TagFacet;
+use MacFJA\RediSearch\Query\Builder\TextFacet;
+use MacFJA\RediSearch\Query\Builder\Word;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Query\Builder
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\AbstractGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\AndGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\ExactMatch
+ * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\Fuzzy
+ * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Negation
+ * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Optional
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\OrGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\Prefix
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElement::priority
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ * @covers \MacFJA\RediSearch\Query\Builder\RawElement
+ * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Word
+ *
+ * @uses \MacFJA\RediSearch\Query\Escaper
+ *
+ * @internal
+ */
+class IncludeSpaceTest extends \PHPUnit\Framework\TestCase
+{
+    public function testGroupIncludeSpace(): void
+    {
+        $andGroup = new AndGroup();
+        $andGroup2 = new AndGroup([new Word('foo bar')]);
+        $andGroup3 = new AndGroup([new Word('foo bar'), new Word('baz')]);
+        $encapsulationGroup = new EncapsulationGroup('(', ')', new RawElement(''));
+        $encapsulationGroup2 = new EncapsulationGroup('(', ')', new Word('foo'), true);
+        $encapsulationGroup3 = new EncapsulationGroup('(', ')', new Word('foo bar'), true);
+        $orGroup = new OrGroup();
+        $orGroup2 = new OrGroup([new Word('foo bar')]);
+        $orGroup3 = new OrGroup([new Word('foo bar'), new Word('baz')]);
+        $orGroup4 = new OrGroup([new Word('foo')]);
+        $builderGroup = new Builder();
+
+        static::assertFalse($andGroup->includeSpace());
+        static::assertTrue($andGroup2->includeSpace());
+        static::assertTrue($andGroup3->includeSpace());
+        static::assertFalse($encapsulationGroup->includeSpace());
+        static::assertFalse($encapsulationGroup2->includeSpace());
+        static::assertFalse($encapsulationGroup3->includeSpace());
+        static::assertFalse($orGroup->includeSpace());
+        static::assertTrue($orGroup2->includeSpace());
+        static::assertTrue($orGroup3->includeSpace());
+        static::assertFalse($orGroup4->includeSpace());
+        static::assertFalse($builderGroup->includeSpace());
+    }
+
+    public function testFacetIncludeSpace(): void
+    {
+        $field = new FieldFacet(['foo'], new RawElement(''));
+        $geo = new GeoFacet(['foo'], 0, 0, 0, GeoFilterOption::UNIT_METERS);
+        $numeric = new NumericFacet(['foo'], 0, 0);
+        $tag = new TagFacet(['foo'], '');
+        $text = new TextFacet(['foo'], '');
+        $text2 = new TextFacet(['foo'], 'bar baz');
+        $text3 = new TextFacet(['foo'], 'bar', 'baz');
+
+        static::assertFalse($field->includeSpace());
+        static::assertFalse($geo->includeSpace());
+        static::assertFalse($numeric->includeSpace());
+        static::assertFalse($tag->includeSpace());
+        static::assertFalse($text->includeSpace());
+        static::assertFalse($text2->includeSpace());
+        static::assertFalse($text3->includeSpace());
+    }
+
+    public function testModifierIncludeSpace(): void
+    {
+        $exactMatch = new ExactMatch('foo bar');
+        $fuzzy = new Fuzzy('foo bar');
+        $negation = new Negation(new RawElement('foo bar'));
+        $optional = new Optional(new RawElement('foo bar'));
+        $prefix = new Prefix('foo bar');
+        $attribute = new QueryElementAttribute(new RawElement('foo bar'));
+        $raw = new RawElement('foo bar');
+        $word = new Word('foo bar');
+
+        static::assertFalse($exactMatch->includeSpace());
+        static::assertTrue($fuzzy->includeSpace());
+        static::assertFalse($negation->includeSpace());
+        static::assertFalse($optional->includeSpace());
+        static::assertFalse($prefix->includeSpace());
+        static::assertTrue($attribute->includeSpace());
+        static::assertTrue($raw->includeSpace());
+        static::assertTrue($word->includeSpace());
+    }
+}
diff --git a/tests/Query/Builder/PriorityTest.php b/tests/Query/Builder/PriorityTest.php
new file mode 100644
index 0000000..4d6f4cc
--- /dev/null
+++ b/tests/Query/Builder/PriorityTest.php
@@ -0,0 +1,122 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query\Builder;
+
+use MacFJA\RediSearch\Query\Builder;
+use MacFJA\RediSearch\Query\Builder\AndGroup;
+use MacFJA\RediSearch\Query\Builder\EncapsulationGroup;
+use MacFJA\RediSearch\Query\Builder\ExactMatch;
+use MacFJA\RediSearch\Query\Builder\FieldFacet;
+use MacFJA\RediSearch\Query\Builder\Fuzzy;
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use MacFJA\RediSearch\Query\Builder\Negation;
+use MacFJA\RediSearch\Query\Builder\NumericFacet;
+use MacFJA\RediSearch\Query\Builder\Optional;
+use MacFJA\RediSearch\Query\Builder\OrGroup;
+use MacFJA\RediSearch\Query\Builder\Prefix;
+use MacFJA\RediSearch\Query\Builder\QueryElement;
+use MacFJA\RediSearch\Query\Builder\QueryElementAttribute;
+use MacFJA\RediSearch\Query\Builder\RawElement;
+use MacFJA\RediSearch\Query\Builder\TagFacet;
+use MacFJA\RediSearch\Query\Builder\TextFacet;
+use MacFJA\RediSearch\Query\Builder\Word;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+
+/**
+ * @covers \MacFJA\RediSearch\Query\Builder
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\AbstractGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\AndGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\ExactMatch
+ * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\Fuzzy
+ * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Negation
+ * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Optional
+ *
+ * @covers \MacFJA\RediSearch\Query\Builder\OrGroup
+ * @covers \MacFJA\RediSearch\Query\Builder\Prefix
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElement::priority
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ * @covers \MacFJA\RediSearch\Query\Builder\RawElement
+ * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
+ * @covers \MacFJA\RediSearch\Query\Builder\Word
+ *
+ * @uses \MacFJA\RediSearch\Query\Escaper
+ *
+ * @internal
+ */
+class PriorityTest extends \PHPUnit\Framework\TestCase
+{
+    public function testGroupPriority(): void
+    {
+        $andGroup = new AndGroup();
+        $encapsulationGroup = new EncapsulationGroup('(', ')', new RawElement(''));
+        $orGroup = new OrGroup();
+        $builderGroup = new Builder();
+
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $andGroup->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $encapsulationGroup->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $orGroup->priority());
+        static::assertSame(QueryElement::PRIORITY_BEFORE, $builderGroup->priority());
+    }
+
+    public function testFacetPriority(): void
+    {
+        $field = new FieldFacet(['foo'], new RawElement(''));
+        $geo = new GeoFacet(['foo'], 0, 0, 0, GeoFilterOption::UNIT_METERS);
+        $numeric = new NumericFacet(['foo'], 0, 0);
+        $tag = new TagFacet(['foo'], '');
+        $text = new TextFacet(['foo'], '');
+
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $field->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $geo->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $numeric->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $tag->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $text->priority());
+    }
+
+    public function testModifierPriority(): void
+    {
+        $exactMatch = new ExactMatch('foo');
+        $fuzzy = new Fuzzy('foo');
+        $negation = new Negation(new RawElement('foo'));
+        $optional = new Optional(new RawElement('foo'));
+        $prefix = new Prefix('foo');
+        $attribute = new QueryElementAttribute(new RawElement('foo'));
+        $raw = new RawElement('foo');
+        $word = new Word('foo');
+
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $exactMatch->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $fuzzy->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $negation->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $optional->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $prefix->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $attribute->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $raw->priority());
+        static::assertSame(QueryElement::PRIORITY_NORMAL, $word->priority());
+    }
+}

From c51d0d195ccf8193050fa66a46f4f52b1486e023 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Oct 2021 15:16:26 +0200
Subject: [PATCH 08/60] Remove mutation testing

---
 CONTRIBUTING.md | 2 --
 1 file changed, 2 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fdd742a..e6ab3f3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -44,9 +44,7 @@ The command will output any information worth knowing. No error should be left.
 If Xdebug is installed, you can also run the commands:
 ```sh
 make coverage
-make mutation-test
 ```
-Ideally, coverage should only increase, and **Covered Code MSI** shouldn't be below 95%
 
 ----
 

From 4383b4657a097c710aa49855c68649ea7676b6cc Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 10 Oct 2021 10:37:21 +0200
Subject: [PATCH 09/60] Use constant instead of hardcoded version

---
 src/Index.php                         | 3 ++-
 src/IndexBuilder.php                  | 5 +++--
 src/Redis/Command/AbstractCommand.php | 2 +-
 src/Redis/Command/Aggregate.php       | 2 +-
 src/Redis/Command/AliasAdd.php        | 2 +-
 src/Redis/Command/AliasDel.php        | 2 +-
 src/Redis/Command/AliasUpdate.php     | 2 +-
 src/Redis/Command/Alter.php           | 2 +-
 src/Redis/Command/Config.php          | 2 +-
 src/Redis/Command/ConfigSet.php       | 2 +-
 src/Redis/Command/Create.php          | 2 +-
 src/Redis/Command/CursorDel.php       | 2 +-
 src/Redis/Command/CursorRead.php      | 4 ++--
 src/Redis/Command/DictAdd.php         | 2 +-
 src/Redis/Command/DictDel.php         | 2 +-
 src/Redis/Command/DictDump.php        | 2 +-
 src/Redis/Command/DropIndex.php       | 2 +-
 src/Redis/Command/Explain.php         | 2 +-
 src/Redis/Command/Info.php            | 2 +-
 src/Redis/Command/Search.php          | 2 +-
 src/Redis/Command/SpellCheck.php      | 2 +-
 src/Redis/Command/SugAdd.php          | 2 +-
 src/Redis/Command/SugDel.php          | 2 +-
 src/Redis/Command/SugGet.php          | 2 +-
 src/Redis/Command/SugLen.php          | 2 +-
 src/Redis/Command/SynDump.php         | 2 +-
 src/Redis/Command/SynUpdate.php       | 2 +-
 src/Redis/Command/TagVals.php         | 2 +-
 src/Redis/Response/CursorResponse.php | 3 ++-
 src/Suggestions.php                   | 3 ++-
 30 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/src/Index.php b/src/Index.php
index b819841..318bc25 100644
--- a/src/Index.php
+++ b/src/Index.php
@@ -24,6 +24,7 @@
 use function count;
 use function is_array;
 use function is_string;
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
 use MacFJA\RediSearch\Redis\Command\AliasDel;
 use MacFJA\RediSearch\Redis\Command\AliasUpdate;
@@ -54,7 +55,7 @@ public function __construct(string $index, ClientInterface $client)
     {
         $this->client = $client;
         $this->index = $index;
-        $this->version = Initializer::getRediSearchVersion($client) ?? '2.0.0';
+        $this->version = Initializer::getRediSearchVersion($client) ?? AbstractCommand::MIN_IMPLEMENTED_VERSION;
         $this->getInfo();
     }
 
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 456c057..7b7ab60 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -29,6 +29,7 @@
 use function is_float;
 use function is_int;
 use function is_string;
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
 use MacFJA\RediSearch\Redis\Command\Create;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
@@ -167,7 +168,7 @@ public function addField(CreateCommandFieldOption ...$option): self
         return $this;
     }
 
-    public function create(ClientInterface $client, string $rediSearchVersion = '2.0.0'): ResponseInterface
+    public function create(ClientInterface $client, string $rediSearchVersion = AbstractCommand::MIN_IMPLEMENTED_VERSION): ResponseInterface
     {
         $command = $this->getCommand($rediSearchVersion);
 
@@ -180,7 +181,7 @@ public function create(ClientInterface $client, string $rediSearchVersion = '2.0
      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
      * @SuppressWarnings(PHPMD.NPathComplexity)
      */
-    public function getCommand(string $rediSearchVersion = '2.0.0'): Create
+    public function getCommand(string $rediSearchVersion = AbstractCommand::MIN_IMPLEMENTED_VERSION): Create
     {
         if (!is_string($this->index)) {
             throw new RuntimeException('The index name is missing');
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index c28d5da..497fba4 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -46,7 +46,7 @@ abstract class AbstractCommand extends Command
     /**
      * @param array<array<CommandOption>|CommandOption|mixed> $options
      */
-    public function __construct(array $options, string $rediSearchVersion = '2.0.0')
+    public function __construct(array $options, string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         $this->rediSearchVersion = $rediSearchVersion;
 
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index fbaac26..2a70626 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -43,7 +43,7 @@ class Aggregate extends AbstractCommand implements PaginatedCommand
 {
     use ArrayResponseTrait;
 
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/AliasAdd.php b/src/Redis/Command/AliasAdd.php
index bd0d78b..fc7f58f 100644
--- a/src/Redis/Command/AliasAdd.php
+++ b/src/Redis/Command/AliasAdd.php
@@ -29,7 +29,7 @@
  */
 class AliasAdd extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'alias' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/AliasDel.php b/src/Redis/Command/AliasDel.php
index e16c9c1..2c56470 100644
--- a/src/Redis/Command/AliasDel.php
+++ b/src/Redis/Command/AliasDel.php
@@ -29,7 +29,7 @@
  */
 class AliasDel extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'alias' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/AliasUpdate.php b/src/Redis/Command/AliasUpdate.php
index ecb4baf..ebdacac 100644
--- a/src/Redis/Command/AliasUpdate.php
+++ b/src/Redis/Command/AliasUpdate.php
@@ -29,7 +29,7 @@
  */
 class AliasUpdate extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'alias' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/Alter.php b/src/Redis/Command/Alter.php
index 4546a1e..ab792d2 100644
--- a/src/Redis/Command/Alter.php
+++ b/src/Redis/Command/Alter.php
@@ -32,7 +32,7 @@ class Alter extends AbstractCommand
 {
     use AddFieldOptionTrait;
 
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/Config.php b/src/Redis/Command/Config.php
index bc47740..dace6ed 100644
--- a/src/Redis/Command/Config.php
+++ b/src/Redis/Command/Config.php
@@ -29,7 +29,7 @@
  */
 class Config extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'action' => CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.0'), ['GET', 'HELP']),
diff --git a/src/Redis/Command/ConfigSet.php b/src/Redis/Command/ConfigSet.php
index db1ffca..f4c3a4e 100644
--- a/src/Redis/Command/ConfigSet.php
+++ b/src/Redis/Command/ConfigSet.php
@@ -31,7 +31,7 @@
  */
 class ConfigSet extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'action' => new FlagOption('SET', true, '>=2.0.0'),
diff --git a/src/Redis/Command/Create.php b/src/Redis/Command/Create.php
index a8d05a2..3fbb584 100644
--- a/src/Redis/Command/Create.php
+++ b/src/Redis/Command/Create.php
@@ -37,7 +37,7 @@ class Create extends AbstractCommand
     use LanguageOptionTrait;
     use AddFieldOptionTrait;
 
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/CursorDel.php b/src/Redis/Command/CursorDel.php
index 9bee9be..8edd956 100644
--- a/src/Redis/Command/CursorDel.php
+++ b/src/Redis/Command/CursorDel.php
@@ -30,7 +30,7 @@
  */
 class CursorDel extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'type' => new FlagOption('DEL', true, '>=2.0.0'),
diff --git a/src/Redis/Command/CursorRead.php b/src/Redis/Command/CursorRead.php
index b037272..c289dd5 100644
--- a/src/Redis/Command/CursorRead.php
+++ b/src/Redis/Command/CursorRead.php
@@ -39,7 +39,7 @@ class CursorRead extends AbstractCommand
 {
     use ArrayResponseTrait;
 
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'type' => new FlagOption('READ', true, '>=2.0.0'),
@@ -85,7 +85,7 @@ public function getId()
     /**
      * @param array<array<mixed>|int> $data
      */
-    public static function transformResponse(array $data, ?int $count, string $index, string $rediSearchVersion = '2.0.0'): CursorResponse
+    public static function transformResponse(array $data, ?int $count, string $index, string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION): CursorResponse
     {
         [$rawResult, $cursorId] = $data;
 
diff --git a/src/Redis/Command/DictAdd.php b/src/Redis/Command/DictAdd.php
index c876a04..6639666 100644
--- a/src/Redis/Command/DictAdd.php
+++ b/src/Redis/Command/DictAdd.php
@@ -28,7 +28,7 @@
  */
 class DictAdd extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dict' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/DictDel.php b/src/Redis/Command/DictDel.php
index c4e49e8..9d55062 100644
--- a/src/Redis/Command/DictDel.php
+++ b/src/Redis/Command/DictDel.php
@@ -28,7 +28,7 @@
  */
 class DictDel extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dict' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/DictDump.php b/src/Redis/Command/DictDump.php
index f8a9750..126676e 100644
--- a/src/Redis/Command/DictDump.php
+++ b/src/Redis/Command/DictDump.php
@@ -28,7 +28,7 @@
  */
 class DictDump extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dict' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/DropIndex.php b/src/Redis/Command/DropIndex.php
index 890b1c7..61e03d6 100644
--- a/src/Redis/Command/DropIndex.php
+++ b/src/Redis/Command/DropIndex.php
@@ -30,7 +30,7 @@
  */
 class DropIndex extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/Explain.php b/src/Redis/Command/Explain.php
index 5be51eb..f629718 100644
--- a/src/Redis/Command/Explain.php
+++ b/src/Redis/Command/Explain.php
@@ -28,7 +28,7 @@
  */
 class Explain extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/Info.php b/src/Redis/Command/Info.php
index b736bfa..8270c60 100644
--- a/src/Redis/Command/Info.php
+++ b/src/Redis/Command/Info.php
@@ -29,7 +29,7 @@
  */
 class Info extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 44da21b..cdb65c7 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -50,7 +50,7 @@ class Search extends AbstractCommand implements PaginatedCommand
     use LanguageOptionTrait;
     use ArrayResponseTrait;
 
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct(
             [
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
index e080586..565f0dd 100644
--- a/src/Redis/Command/SpellCheck.php
+++ b/src/Redis/Command/SpellCheck.php
@@ -34,7 +34,7 @@
  */
 class SpellCheck extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/SugAdd.php b/src/Redis/Command/SugAdd.php
index 3814046..2b84143 100644
--- a/src/Redis/Command/SugAdd.php
+++ b/src/Redis/Command/SugAdd.php
@@ -31,7 +31,7 @@
  */
 class SugAdd extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dictionary' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/SugDel.php b/src/Redis/Command/SugDel.php
index d57d719..466ea98 100644
--- a/src/Redis/Command/SugDel.php
+++ b/src/Redis/Command/SugDel.php
@@ -28,7 +28,7 @@
  */
 class SugDel extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dictionary' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/SugGet.php b/src/Redis/Command/SugGet.php
index 1edbb6c..6bba1a0 100644
--- a/src/Redis/Command/SugGet.php
+++ b/src/Redis/Command/SugGet.php
@@ -35,7 +35,7 @@
  */
 class SugGet extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dictionary' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/SugLen.php b/src/Redis/Command/SugLen.php
index 2d033b6..6247820 100644
--- a/src/Redis/Command/SugLen.php
+++ b/src/Redis/Command/SugLen.php
@@ -28,7 +28,7 @@
  */
 class SugLen extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'dictionary' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/SynDump.php b/src/Redis/Command/SynDump.php
index aeec8cc..36190bf 100644
--- a/src/Redis/Command/SynDump.php
+++ b/src/Redis/Command/SynDump.php
@@ -25,7 +25,7 @@
 
 class SynDump extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/SynUpdate.php b/src/Redis/Command/SynUpdate.php
index e4141da..a581c16 100644
--- a/src/Redis/Command/SynUpdate.php
+++ b/src/Redis/Command/SynUpdate.php
@@ -26,7 +26,7 @@
 
 class SynUpdate extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Command/TagVals.php b/src/Redis/Command/TagVals.php
index d8f3d6c..5024d43 100644
--- a/src/Redis/Command/TagVals.php
+++ b/src/Redis/Command/TagVals.php
@@ -28,7 +28,7 @@
  */
 class TagVals extends AbstractCommand
 {
-    public function __construct(string $rediSearchVersion = '2.0.0')
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index a55dee4..1ad8fa2 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -23,6 +23,7 @@
 
 use function count;
 use Iterator;
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\CursorRead;
 use Predis\ClientInterface;
 use Predis\Response\ResponseInterface;
@@ -54,7 +55,7 @@ class CursorResponse implements ResponseInterface, Iterator
     /**
      * @param array<AggregateResponseItem> $items
      */
-    public function __construct(int $cursorId, int $totalCount, array $items, int $size, string $index, string $redisVersion = '2.0.0')
+    public function __construct(int $cursorId, int $totalCount, array $items, int $size, string $index, string $redisVersion = AbstractCommand::MIN_IMPLEMENTED_VERSION)
     {
         $this->totalCount = $totalCount;
         $this->items = $items;
diff --git a/src/Suggestions.php b/src/Suggestions.php
index 5403d3f..52649b3 100644
--- a/src/Suggestions.php
+++ b/src/Suggestions.php
@@ -23,6 +23,7 @@
 
 use function is_int;
 use function is_string;
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\SugAdd;
 use MacFJA\RediSearch\Redis\Command\SugDel;
 use MacFJA\RediSearch\Redis\Command\SugGet;
@@ -50,7 +51,7 @@ public function __construct(string $dictionary, ClientInterface $client)
         $this->dictionary = $dictionary;
         $this->client = $client;
         $this->length = $client->executeCommand((new SugLen())->setDictionary($dictionary));
-        $this->version = Initializer::getRediSearchVersion($client) ?? '2.0.0';
+        $this->version = Initializer::getRediSearchVersion($client) ?? AbstractCommand::MIN_IMPLEMENTED_VERSION;
     }
 
     public function add(string $suggestion, float $score, bool $increment = false, ?string $payload = null): void

From 13d3777caf64a66e09935ef921cb7fa1a3691c12 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 12 Oct 2021 00:04:42 +0200
Subject: [PATCH 10/60] Improve coverage + add UT for SpellCheck result parsing

---
 .../AggregateCommand/GroupByOption.php        |  2 ++
 .../Command/AggregateCommand/SortByOption.php |  2 ++
 tests/Redis/Command/SpellCheckTest.php        | 22 +++++++++++++++++++
 3 files changed, 26 insertions(+)

diff --git a/src/Redis/Command/AggregateCommand/GroupByOption.php b/src/Redis/Command/AggregateCommand/GroupByOption.php
index 23ca64e..7985702 100644
--- a/src/Redis/Command/AggregateCommand/GroupByOption.php
+++ b/src/Redis/Command/AggregateCommand/GroupByOption.php
@@ -58,6 +58,8 @@ public function isValid(): bool
     }
 
     /**
+     * @codeCoverageIgnore Simple getter
+     *
      * @return array<string,mixed>
      */
     public function getOptionData()
diff --git a/src/Redis/Command/AggregateCommand/SortByOption.php b/src/Redis/Command/AggregateCommand/SortByOption.php
index 66cc86a..e1a5155 100644
--- a/src/Redis/Command/AggregateCommand/SortByOption.php
+++ b/src/Redis/Command/AggregateCommand/SortByOption.php
@@ -52,6 +52,8 @@ public function isValid(): bool
     }
 
     /**
+     * @codeCoverageIgnore Simple getter
+     *
      * @return array<string,null|array<string,null|string>|int>
      */
     public function getOptionData()
diff --git a/tests/Redis/Command/SpellCheckTest.php b/tests/Redis/Command/SpellCheckTest.php
index f83b9ca..ffa86ca 100644
--- a/tests/Redis/Command/SpellCheckTest.php
+++ b/tests/Redis/Command/SpellCheckTest.php
@@ -21,7 +21,9 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use Closure;
 use MacFJA\RediSearch\Redis\Command\SpellCheck;
+use MacFJA\RediSearch\Redis\Response\SpellCheckResponseItem;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -64,4 +66,24 @@ public function testFullOption(): void
             'TERMS', 'INCLUDE', 'cities',
         ], $command->getArguments());
     }
+
+    public function testResponseTransformation(): void
+    {
+        $command = new SpellCheck();
+        $expected = [
+            new SpellCheckResponseItem('hell', ['hello' => 0.7, 'hola' => 0.4]),
+            new SpellCheckResponseItem('worl', ['world' => 0.9, 'work' => 0.56]),
+        ];
+        $rawResponse = [
+            ['TERM', 'hell', [[0.7, 'hello'], [0.4, 'hola']]],
+            ['TERM', 'worl', [[0.9, 'world'], [0.56, 'work']]],
+        ];
+        $transformer = function ($data) {
+            // @phpstan-ignore-next-line
+            return $this->transformParsedResponse($data);
+        };
+        $actual = Closure::fromCallable($transformer)->call($command, $rawResponse);
+
+        static::assertEquals($expected, $actual);
+    }
 }

From 596d4a708942937d968420cf450afa288002141e Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 12 Oct 2021 00:06:23 +0200
Subject: [PATCH 11/60] Fix error message in command option getter

---
 src/Redis/Command/Option/WithPublicGroupedSetterTrait.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php b/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
index 32f008a..5618171 100644
--- a/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
+++ b/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
@@ -58,7 +58,7 @@ public function __call(string $name, array $arguments): self
             return $this->setDataOfOption($snakeCase, $arguments[0]);
         }
 
-        throw new BadMethodCallException(sprintf('Call undefined method %s is not callable in %s', $name, get_class($this)));
+        throw new BadMethodCallException(sprintf('Call undefined method %s in %s', $name, get_class($this)));
     }
 
     /**

From 79b7281ab9d3293abe7222124957993a1dfa6092 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Fri, 15 Oct 2021 21:37:44 +0200
Subject: [PATCH 12/60] Fix builder magic method call

---
 src/IndexBuilder.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 7b7ab60..80dc7ae 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -136,7 +136,7 @@ class IndexBuilder
      */
     public function __call(string $name, array $arguments): self
     {
-        $calls = ['set' => true, 'add' => false, 'withAdded' => false, 'with' => false];
+        $calls = ['set' => true, 'add' => false, 'withAdded' => false, 'with' => true];
         foreach ($calls as $type => $singleArgument) {
             $result = $this->doCall($type, $name, true === $singleArgument ? $arguments[0] ?? null : $arguments);
             if ($result instanceof IndexBuilder) {
@@ -262,7 +262,7 @@ private function withCall(string $name, $argument): self
     {
         $newInstance = clone $this;
 
-        return $newInstance->{'set'.$name}($name, $argument);
+        return $newInstance->{'set'.$name}($argument);
     }
 
     /**
@@ -276,7 +276,7 @@ private function withAddedCall(string $name, array $arguments): self
     {
         $newInstance = clone $this;
 
-        return $newInstance->{'add'.$name}($name, $arguments);
+        return $newInstance->{'add'.$name}($arguments);
     }
 
     /**

From 101d49c2e4168033cd1ba6c45a432b9d4b083c33 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 16 Oct 2021 14:28:26 +0200
Subject: [PATCH 13/60] Remove hard dependency on Predis + allow other Redis
 lib

---
 .unused.dist.php                              |   1 +
 README.md                                     |  49 +++--
 composer.json                                 |   8 +-
 composer.lock                                 | 186 +++++++++++-------
 phpstan.neon                                  |   3 +
 psalm.xml                                     |   4 +
 src/Index.php                                 |  24 +--
 src/IndexBuilder.php                          |  10 +-
 src/Redis/Client.php                          |  56 ++++++
 src/Redis/Client/ClientFacade.php             |  64 ++++++
 src/Redis/Client/PhpiredisClient.php          | 111 +++++++++++
 src/Redis/Client/PhpredisClient.php           | 106 ++++++++++
 src/Redis/Client/PredisClient.php             | 112 +++++++++++
 src/Redis/Command.php                         |  43 ++++
 src/Redis/Command/AbstractCommand.php         |  92 +++------
 src/Redis/Command/Aggregate.php               |   2 +-
 src/Redis/Command/AliasAdd.php                |   6 +-
 src/Redis/Command/AliasDel.php                |   6 +-
 src/Redis/Command/AliasUpdate.php             |   6 +-
 src/Redis/Command/Alter.php                   |   6 +-
 src/Redis/Command/Config.php                  |   2 +-
 src/Redis/Command/ConfigSet.php               |   6 +-
 src/Redis/Command/Create.php                  |   6 +-
 src/Redis/Command/CursorDel.php               |   6 +-
 src/Redis/Command/CursorRead.php              |   2 +-
 src/Redis/Command/DictAdd.php                 |   2 +-
 src/Redis/Command/DictDel.php                 |   2 +-
 src/Redis/Command/DictDump.php                |   2 +-
 src/Redis/Command/DropIndex.php               |   6 +-
 src/Redis/Command/Explain.php                 |   2 +-
 src/Redis/Command/ExplainCli.php              |   2 +-
 src/Redis/Command/IndexList.php               |  37 +++-
 src/Redis/Command/Info.php                    |   2 +-
 src/Redis/Command/PaginatedCommand.php        |   4 +-
 src/Redis/Command/Search.php                  |   2 +-
 src/Redis/Command/SpellCheck.php              |   2 +-
 src/Redis/Command/SugAdd.php                  |   2 +-
 src/Redis/Command/SugDel.php                  |   2 +-
 src/Redis/Command/SugGet.php                  |   2 +-
 src/Redis/Command/SugLen.php                  |   2 +-
 src/Redis/Command/SynDump.php                 |   2 +-
 src/Redis/Command/SynUpdate.php               |   2 +-
 src/Redis/Command/TagVals.php                 |   2 +-
 src/Redis/Initializer.php                     |   5 +-
 src/Redis/Response.php                        |  26 +++
 src/Redis/Response/CursorResponse.php         |  12 +-
 src/Redis/Response/InfoResponse.php           |  15 +-
 src/Redis/Response/PaginatedResponse.php      |  12 +-
 src/Suggestions.php                           |  14 +-
 tests/Redis/InitializerTest.php               |   9 +-
 .../Redis/Command/FakeAbstractCommand.php     |   2 +-
 tests/psalm/stubs/phpiredis.php               | 155 +++++++++++++++
 52 files changed, 973 insertions(+), 271 deletions(-)
 create mode 100644 src/Redis/Client.php
 create mode 100644 src/Redis/Client/ClientFacade.php
 create mode 100644 src/Redis/Client/PhpiredisClient.php
 create mode 100644 src/Redis/Client/PhpredisClient.php
 create mode 100644 src/Redis/Client/PredisClient.php
 create mode 100644 src/Redis/Command.php
 create mode 100644 src/Redis/Response.php
 create mode 100644 tests/psalm/stubs/phpiredis.php

diff --git a/.unused.dist.php b/.unused.dist.php
index c9ff678..39cc3d7 100644
--- a/.unused.dist.php
+++ b/.unused.dist.php
@@ -28,6 +28,7 @@
         'enlightn/security-checker', // QA tool
         'php-parallel-lint/php-parallel-lint', // QA tool
         'sebastian/phpcpd', // QA tool
+        'ukko/phpredis-phpdoc' // Stubs
     ],
     'excludeDirectories' => [],
     'scanFiles' => [],
diff --git a/README.md b/README.md
index 4a8907b..3018080 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,38 @@ composer require macfja/redisearch
 
 ## Usage
 
+### Get a Redis client
+
+This lib can use several connector for Redis:
+ - [Predis](https://github.com/predis/predis/wiki) - Pure PHP implementation
+ - [Phpredis](https://github.com/phpredis/phpredis) - PHP extension
+ - [Phpiredis](https://github.com/nrk/phpiredis) - PHP extension depending on [hiredis](https://github.com/redis/hiredis)
+
+You can pick the connector depending of your need.
+
+```php
+$clientFacade = new \MacFJA\RediSearch\Redis\Client\ClientFacade();
+
+// With Predis
+$client = $clientFacade->getClient(new \Predis\Client(/* ... */));
+
+// With Phpredis extension
+$client = $clientFacade->getClient(new Redis([/* ... */]));
+
+// With Phpiredis extension
+$client = $clientFacade->getClient(phpiredis_connect($host));
+```
+
+You can add your own implementation, all you need is to implement the interface `\MacFJA\RediSearch\Redis\Client` and add it to the client facace with:
+```php
+$clientFacade = new \MacFJA\RediSearch\Redis\Client\ClientFacade();
+$clientFacade->addFactory(\MyVendor\MyPackage\MyRedisClient::class);
+```
+
 ### Create a new index
 
 ```php
-$client = new \Predis\Client(/* ... */);
+$client = /* ... */;
 $builder = new \MacFJA\RediSearch\IndexBuilder();
 
 // Field can be create in advance
@@ -38,9 +66,9 @@ This will give you a new instance of the builder with the configured data.
 ### Add a document
 
 ```php
-$client = new \Predis\Client(/* ... */);
+$client = /* ... */;
 $index = new \MacFJA\RediSearch\Index('person', $client);
-$index->addFromArray([
+$index->addDocumentFromArray([
     'firstname' => 'Joe',
     'lastname' => 'Doe',
     'age' => 30,
@@ -51,7 +79,7 @@ $index->addFromArray([
 ### Search
 
 ```php
-$client = new \Predis\Client(/* ... */);
+$client = /* ... */;
 $search = new \MacFJA\RediSearch\Redis\Command\Search();
 
 $search
@@ -59,7 +87,7 @@ $search
     ->setQuery('Doe')
     ->setHighlight(['lastname'])
     ->setWithScores();
-$results = $client->executeCommand($search);
+$results = $client->execute($search);
 ```
 
 #### Create a search query
@@ -96,9 +124,8 @@ use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
 use MacFJA\RediSearch\Redis\Command\Search;
 use MacFJA\RediSearch\Redis\Command\SugGet;
-use Predis\Client;
 
-$client = new Client(/* ... */);
+$client = /* ... */;
 
 $query = '@age:[(17 +inf] %john%';
 $search = new Search();
@@ -123,13 +150,7 @@ $suggestion->setDictionary('names')
     ->setPrefix('john')
     ->setFuzzy();
 
-$result = $client->pipeline()
-    ->executeCommand($search)
-    ->executeCommand($stats)
-    ->executeCommand($aggregate)
-    ->executeCommand($suggestion)
-    ->execute()
-;
+$result = $client->pipeline($search, $stats, $aggregate, $suggestion);
 
 // $result[0] is the search result
 // $result[1] is the first aggregation result
diff --git a/composer.json b/composer.json
index 898e756..b8a3d4e 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,6 @@
         "php": "^7.2",
         "ext-intl": "*",
         "composer/semver": "^3.2",
-        "predis/predis": "^1.1",
         "respect/validation": "^2.0"
     },
     "require-dev": {
@@ -31,11 +30,18 @@
         "phpmd/phpmd": "^2.10",
         "phpstan/phpstan": "^0.12.92",
         "phpunit/phpunit": "^8.5",
+        "predis/predis": "^1.1",
         "roave/security-advisories": "dev-latest",
         "rskuipers/php-assumptions": "^0.8.0",
         "sebastian/phpcpd": "^4.1",
+        "ukko/phpredis-phpdoc": "dev-master",
         "vimeo/psalm": "^4.7"
     },
+    "suggest": {
+        "ext-phpiredis": "To use Phpiredis extension implementation",
+        "ext-phpredis": "To use Phpredis extension implementation",
+        "predis/predis": "To use Predis implementation"
+    },
     "config": {
         "platform": {
             "php": "7.2"
diff --git a/composer.lock b/composer.lock
index 9c486a2..6075858 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "9ed68d6965de036262b2505f9aa3eadf",
+    "content-hash": "d631916a7eaceb593add53d973c6cb88",
     "packages": [
         {
             "name": "composer/semver",
@@ -87,72 +87,6 @@
             ],
             "time": "2021-05-24T12:41:47+00:00"
         },
-        {
-            "name": "predis/predis",
-            "version": "v1.1.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/predis/predis.git",
-                "reference": "cf5c118a077fbab8b9af1482c20952173125c041"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/predis/predis/zipball/cf5c118a077fbab8b9af1482c20952173125c041",
-                "reference": "cf5c118a077fbab8b9af1482c20952173125c041",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.9"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.8"
-            },
-            "suggest": {
-                "ext-curl": "Allows access to Webdis when paired with phpiredis",
-                "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Predis\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Daniele Alessandri",
-                    "email": "suppakilla@gmail.com",
-                    "homepage": "http://clorophilla.net",
-                    "role": "Creator & Maintainer"
-                },
-                {
-                    "name": "Till Krüss",
-                    "homepage": "https://till.im",
-                    "role": "Maintainer"
-                }
-            ],
-            "description": "Flexible and feature-complete Redis client for PHP and HHVM",
-            "homepage": "http://github.com/predis/predis",
-            "keywords": [
-                "nosql",
-                "predis",
-                "redis"
-            ],
-            "support": {
-                "issues": "https://github.com/predis/predis/issues",
-                "source": "https://github.com/predis/predis/tree/v1.1.8"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sponsors/tillkruss",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-09-29T17:48:39+00:00"
-        },
         {
             "name": "respect/stringifier",
             "version": "0.2.0",
@@ -3111,6 +3045,72 @@
             ],
             "time": "2021-09-25T07:37:20+00:00"
         },
+        {
+            "name": "predis/predis",
+            "version": "v1.1.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/predis/predis.git",
+                "reference": "cf5c118a077fbab8b9af1482c20952173125c041"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/predis/predis/zipball/cf5c118a077fbab8b9af1482c20952173125c041",
+                "reference": "cf5c118a077fbab8b9af1482c20952173125c041",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.8"
+            },
+            "suggest": {
+                "ext-curl": "Allows access to Webdis when paired with phpiredis",
+                "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Predis\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniele Alessandri",
+                    "email": "suppakilla@gmail.com",
+                    "homepage": "http://clorophilla.net",
+                    "role": "Creator & Maintainer"
+                },
+                {
+                    "name": "Till Krüss",
+                    "homepage": "https://till.im",
+                    "role": "Maintainer"
+                }
+            ],
+            "description": "Flexible and feature-complete Redis client for PHP and HHVM",
+            "homepage": "http://github.com/predis/predis",
+            "keywords": [
+                "nosql",
+                "predis",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/predis/predis/issues",
+                "source": "https://github.com/predis/predis/tree/v1.1.8"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sponsors/tillkruss",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-09-29T17:48:39+00:00"
+        },
         {
             "name": "psr/cache",
             "version": "1.0.1",
@@ -6240,6 +6240,55 @@
             ],
             "time": "2021-07-28T10:34:58+00:00"
         },
+        {
+            "name": "ukko/phpredis-phpdoc",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ukko/phpredis-phpdoc.git",
+                "reference": "5ce7ea21e7240f4c60ab9a0f5a9208ea6c2a2f30"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ukko/phpredis-phpdoc/zipball/5ce7ea21e7240f4c60ab9a0f5a9208ea6c2a2f30",
+                "reference": "5ce7ea21e7240f4c60ab9a0f5a9208ea6c2a2f30",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "default-branch": true,
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "phpredis-phpdoc": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Max Kamashev",
+                    "email": "max.kamashev@gmail.com",
+                    "homepage": "http://max.kamashev.name/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "@phpdoc extension phpredis for IDE autocomplete",
+            "homepage": "https://github.com/ukko/phpredis-phpdoc",
+            "keywords": [
+                "helper",
+                "phpredis",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/ukko/phpredis-phpdoc/issues",
+                "source": "https://github.com/ukko/phpredis-phpdoc/tree/master"
+            },
+            "time": "2021-05-08T16:57:08+00:00"
+        },
         {
             "name": "vimeo/psalm",
             "version": "4.10.0",
@@ -6457,7 +6506,8 @@
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
-        "roave/security-advisories": 20
+        "roave/security-advisories": 20,
+        "ukko/phpredis-phpdoc": 20
     },
     "prefer-stable": false,
     "prefer-lowest": false,
diff --git a/phpstan.neon b/phpstan.neon
index 6900a90..ee1a995 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -3,6 +3,9 @@ parameters:
     paths:
         - src
         - tests
+    excludePaths:
+        analyse:
+            - tests/psalm/stubs/phpiredis.php
     ignoreErrors:
         - '#Method MacFJA\\RediSearch\\Redis\\Command\\\w+::getLanguageOptions\(\) should return#'
         - '#uses generic trait MacFJA\\RediSearch\\Redis\\Command\\Option\\DecoratedOptionTrait but does not specify its types: T#'
diff --git a/psalm.xml b/psalm.xml
index 7c0333d..429bc62 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -12,4 +12,8 @@
             <directory name="vendor" />
         </ignoreFiles>
     </projectFiles>
+    <stubs>
+        <file name="vendor/ukko/phpredis-phpdoc/src/Redis.php"/>
+        <file name="tests/psalm/stubs/phpiredis.php"/>
+    </stubs>
 </psalm>
diff --git a/src/Index.php b/src/Index.php
index 318bc25..eb4c225 100644
--- a/src/Index.php
+++ b/src/Index.php
@@ -24,6 +24,7 @@
 use function count;
 use function is_array;
 use function is_string;
+use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
 use MacFJA\RediSearch\Redis\Command\AliasDel;
@@ -35,14 +36,13 @@
 use MacFJA\RediSearch\Redis\Command\TagVals;
 use MacFJA\RediSearch\Redis\Initializer;
 use MacFJA\RediSearch\Redis\Response\InfoResponse;
-use Predis\ClientInterface;
 
 /**
  * @codeCoverageIgnore
  */
 class Index
 {
-    /** @var ClientInterface */
+    /** @var Client */
     private $client;
     /** @var InfoResponse */
     private $info;
@@ -51,7 +51,7 @@ class Index
     /** @var string */
     private $version;
 
-    public function __construct(string $index, ClientInterface $client)
+    public function __construct(string $index, Client $client)
     {
         $this->client = $client;
         $this->index = $index;
@@ -76,14 +76,14 @@ public function addDocumentFromArray(array $properties, ?string $hash = null): s
             $query[] = $value;
         }
 
-        $this->client->hset(...$query);
+        $this->client->executeRaw('hset', ...$query);
 
         return $documentId;
     }
 
     public function deleteDocument(string $hash): bool
     {
-        $count = $this->client->del($hash);
+        $count = $this->client->executeRaw('del', $hash);
 
         return 1 === $count;
     }
@@ -96,7 +96,7 @@ public function addField(CreateCommandFieldOption $field): bool
             ->addField($field)
         ;
 
-        return 'OK' === (string) $this->client->executeCommand($command);
+        return 'OK' === (string) $this->client->execute($command);
     }
 
     public function delete(bool $withDocuments = false): bool
@@ -106,12 +106,12 @@ public function delete(bool $withDocuments = false): bool
             ->setDeleteDocument($withDocuments)
         ;
 
-        return 'OK' === (string) $this->client->executeCommand($command);
+        return 'OK' === (string) $this->client->execute($command);
     }
 
     public function addAlias(string $alias): bool
     {
-        return 'OK' === (string) $this->client->executeCommand(
+        return 'OK' === (string) $this->client->execute(
             (new AliasAdd($this->version))
                 ->setIndex($this->index)
                 ->setAlias($alias)
@@ -120,12 +120,12 @@ public function addAlias(string $alias): bool
 
     public function updateAlias(string $alias): bool
     {
-        return 'OK' === (string) $this->client->executeCommand((new AliasUpdate($this->version))->setIndex($this->index)->setAlias($alias));
+        return 'OK' === (string) $this->client->execute((new AliasUpdate($this->version))->setIndex($this->index)->setAlias($alias));
     }
 
     public function deleteAlias(string $alias): bool
     {
-        return 'OK' === (string) $this->client->executeCommand((new AliasDel($this->version))->setAlias($alias));
+        return 'OK' === (string) $this->client->execute((new AliasDel($this->version))->setAlias($alias));
     }
 
     /**
@@ -133,12 +133,12 @@ public function deleteAlias(string $alias): bool
      */
     public function getTagValues(string $fieldName): array
     {
-        return $this->client->executeCommand((new TagVals($this->version))->setIndex($this->index)->setField($fieldName));
+        return $this->client->execute((new TagVals($this->version))->setIndex($this->index)->setField($fieldName));
     }
 
     public function getInfo(): InfoResponse
     {
-        $this->info = $this->client->executeCommand((new Info($this->version))->setIndex($this->index));
+        $this->info = $this->client->execute((new Info($this->version))->setIndex($this->index));
 
         return $this->info;
     }
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 80dc7ae..4b547b7 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -29,12 +29,11 @@
 use function is_float;
 use function is_int;
 use function is_string;
+use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
 use MacFJA\RediSearch\Redis\Command\Create;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
-use Predis\ClientInterface;
-use Predis\Response\ResponseInterface;
 use RuntimeException;
 use function strlen;
 
@@ -168,11 +167,14 @@ public function addField(CreateCommandFieldOption ...$option): self
         return $this;
     }
 
-    public function create(ClientInterface $client, string $rediSearchVersion = AbstractCommand::MIN_IMPLEMENTED_VERSION): ResponseInterface
+    /**
+     * @return mixed|string
+     */
+    public function create(Client $client, string $rediSearchVersion = AbstractCommand::MIN_IMPLEMENTED_VERSION)
     {
         $command = $this->getCommand($rediSearchVersion);
 
-        return $client->executeCommand($command);
+        return $client->execute($command);
     }
 
     /**
diff --git a/src/Redis/Client.php b/src/Redis/Client.php
new file mode 100644
index 0000000..ee94663
--- /dev/null
+++ b/src/Redis/Client.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis;
+
+interface Client
+{
+    /**
+     * @param mixed $redis
+     */
+    public static function make($redis): Client;
+
+    /**
+     * @ return array<mixed>|int|Response|string
+     *
+     * @return mixed
+     */
+    public function execute(Command $command);
+
+    /**
+     * @param float|int|string $args
+     *
+     * @return mixed
+     */
+    public function executeRaw(...$args);
+
+    /**
+     * @param Command ...$commands
+     *
+     * @return array<mixed>
+     */
+    public function pipeline(Command ...$commands): array;
+
+    /**
+     * @param mixed $redis
+     */
+    public static function supports($redis): bool;
+}
diff --git a/src/Redis/Client/ClientFacade.php b/src/Redis/Client/ClientFacade.php
new file mode 100644
index 0000000..3b1a276
--- /dev/null
+++ b/src/Redis/Client/ClientFacade.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use MacFJA\RediSearch\Redis\Client;
+use Predis\ClientInterface;
+use Redis;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class ClientFacade
+{
+    /** @var array<string> */
+    private $factories = [PhpiredisClient::class, PhpredisClient::class, PredisClient::class];
+
+    /**
+     * @param ClientInterface|mixed|Redis|resource $redis
+     */
+    public function getClient($redis): Client
+    {
+        /** @var Client $factory */
+        foreach ($this->factories as $factory) {
+            if (!is_subclass_of($factory, Client::class)) {
+                continue;
+            }
+            if ($factory::supports($redis)) {
+                return $factory::make($redis);
+            }
+        }
+
+        throw new RuntimeException('Unable to handle the Redis connection');
+    }
+
+    public function addFactory(string $classname): bool
+    {
+        if (!is_subclass_of($classname, Client::class)) {
+            return false;
+        }
+        $this->factories[] = $classname;
+
+        return true;
+    }
+}
diff --git a/src/Redis/Client/PhpiredisClient.php b/src/Redis/Client/PhpiredisClient.php
new file mode 100644
index 0000000..ef98cd2
--- /dev/null
+++ b/src/Redis/Client/PhpiredisClient.php
@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use Exception;
+use function function_exists;
+use function is_resource;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class PhpiredisClient implements Client
+{
+    /** @var resource */
+    private $redis;
+
+    /**
+     * @param resource $redis
+     */
+    private function __construct($redis)
+    {
+        $this->validateEnvironment();
+        $this->redis = $redis;
+    }
+
+    public function execute(Command $command)
+    {
+        $this->validateEnvironment();
+        $rawResponse = phpiredis_command_bs($this->redis, array_merge([$command->getId()], $command->getArguments()));
+
+        return $command->parseResponse($rawResponse);
+    }
+
+    public static function supports($redis): bool
+    {
+        if (!is_resource($redis)
+            || !function_exists('phpiredis_command_bs')
+            || !function_exists('phpiredis_multi_command_bs')
+        ) {
+            return false;
+        }
+
+        try {
+            $response = (string) phpiredis_command_bs($redis, ['PING']);
+
+            return 'pong' === strtolower($response);
+        } catch (Exception $exception) {
+            return false;
+        }
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    public function executeRaw(...$args)
+    {
+        $this->validateEnvironment();
+
+        $args = array_map('strval', $args);
+
+        return phpiredis_command_bs($this->redis, $args);
+    }
+
+    public function pipeline(Command ...$commands): array
+    {
+        $results = phpiredis_multi_command_bs($this->redis, array_map(static function (Command $command) {
+            return array_merge([$command->getId()], $command->getArguments());
+        }, $commands));
+
+        return array_map(static function ($result, $index) use ($commands) {
+            return $commands[$index]->parseResponse($result);
+        }, $results, array_keys($results));
+    }
+
+    /**
+     * @psalm-assert function_exists('phpiredis_command')
+     */
+    private function validateEnvironment(): void
+    {
+        if (!function_exists('phpiredis_command_bs') || !function_exists('phpiredis_multi_command_bs')) {
+            throw new RuntimeException(
+                'The extension phpiredis is missing.'.PHP_EOL.
+                'Install the extension or use a polyfill that provide the functions "phpiredis_command_bs" and "phpiredis_multi_command_bs"'
+            );
+        }
+    }
+}
diff --git a/src/Redis/Client/PhpredisClient.php b/src/Redis/Client/PhpredisClient.php
new file mode 100644
index 0000000..0f0d406
--- /dev/null
+++ b/src/Redis/Client/PhpredisClient.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use function count;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use Redis;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class PhpredisClient implements Client
+{
+    /** @var Redis */
+    private $redis;
+
+    private function __construct(Redis $redis)
+    {
+        if (
+            !method_exists(Redis::class, 'rawCommand')
+            || !method_exists(Redis::class, 'multi')
+            || !method_exists(Redis::class, 'exec')
+        ) {
+            throw new RuntimeException(
+                'The extension phpredis is missing.'.PHP_EOL.
+                'Install the extension or use a polyfill that provide the class "\\Redis" and the functions '.
+                '"\\Redis::rawCommand" and "\\Redis::multi" and "\\Redis::exec"'
+            );
+        }
+        $this->redis = $redis;
+    }
+
+    public function execute(Command $command)
+    {
+        $arguments = $command->getArguments();
+        if (0 === count($arguments)) {
+            $arguments = [null];
+        }
+        $rawResponse = $this->redis->rawCommand($command->getId(), ...$arguments);
+
+        return $command->parseResponse($rawResponse);
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof Redis
+            && method_exists($redis, 'rawCommand')
+            && method_exists($redis, 'multi')
+            && method_exists($redis, 'exec');
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    public function executeRaw(...$args)
+    {
+        if (count($args) < 1) {
+            return null;
+        }
+        if (count($args) < 2) {
+            $args[] = null;
+        }
+        // @phpstan-ignore-next-line
+        return $this->redis->rawCommand(...$args);
+    }
+
+    public function pipeline(Command ...$commands): array
+    {
+        $pipeline = $this->redis->multi();
+        foreach ($commands as $command) {
+            $arguments = $command->getArguments();
+            if (0 === count($arguments)) {
+                $arguments = [null];
+            }
+            $pipeline = $pipeline->rawCommand($command->getId(), ...$arguments);
+        }
+        $results = $pipeline->exec();
+
+        return array_map(static function ($result, $index) use ($commands) {
+            return $commands[$index]->parseResponse($result);
+        }, $results, array_keys($results));
+    }
+}
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
new file mode 100644
index 0000000..9412b86
--- /dev/null
+++ b/src/Redis/Client/PredisClient.php
@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use function assert;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use Predis\ClientContextInterface;
+use Predis\ClientInterface;
+use Predis\Command\RawCommand;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class PredisClient implements Client
+{
+    /** @var ClientInterface */
+    private $redis;
+
+    private function __construct(ClientInterface $redis)
+    {
+        if (
+            !static::fcqnExists(\Predis\Command\Command::class)
+            || !static::fcqnExists(ClientContextInterface::class)
+            || !method_exists(ClientInterface::class, 'executeCommand')
+            || !method_exists($redis, 'pipeline')
+            || !method_exists(ClientContextInterface::class, 'executeCommand')
+        ) {
+            throw new RuntimeException(
+                'Dependency Predis is missing.'.PHP_EOL.
+                'Install the dependency or use a polyfill that provide the classes'.
+                ' "\\Predis\\Command\\Command" and "\\Predis\\ClientInterface" and "\\Predis\\ClientContextInterface"'.
+                ' and functions "\\Predis\\ClientInterface::executeCommand" and "\\Predis\\ClientInterface::pipeline"'.
+                ' and "\\Predis\\ClientContextInterface::executeCommand"'
+            );
+        }
+        $this->redis = $redis;
+    }
+
+    public function execute(Command $command)
+    {
+        $rawResponse = $this->redis->executeCommand(new RawCommand(array_merge([$command->getId()], $command->getArguments())));
+
+        return $command->parseResponse($rawResponse);
+    }
+
+    public static function supports($redis): bool
+    {
+        if (
+            !static::fcqnExists(ClientInterface::class)
+            || !static::fcqnExists(RawCommand::class)
+        ) {
+            return false;
+        }
+
+        return $redis instanceof ClientInterface
+            && method_exists($redis, 'executeCommand')
+            && method_exists($redis, 'pipeline');
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    /**
+     * @inheridoc
+     */
+    public function executeRaw(...$args)
+    {
+        return $this->redis->executeCommand(new RawCommand($args));
+    }
+
+    public function pipeline(Command ...$commands): array
+    {
+        assert(method_exists($this->redis, 'pipeline'));
+        $results = $this->redis->pipeline(static function (ClientContextInterface $pipeline) use ($commands): void {
+            foreach ($commands as $command) {
+                $pipeline->executeCommand(new RawCommand(array_merge([$command->getId()], $command->getArguments())));
+            }
+        });
+
+        return array_map(static function ($result, $index) use ($commands) {
+            return $commands[$index]->parseResponse($result);
+        }, $results, array_keys($results));
+    }
+
+    private static function fcqnExists(string $fcqn): bool
+    {
+        return class_exists($fcqn) || interface_exists($fcqn);
+    }
+}
diff --git a/src/Redis/Command.php b/src/Redis/Command.php
new file mode 100644
index 0000000..3797483
--- /dev/null
+++ b/src/Redis/Command.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis;
+
+interface Command
+{
+    public function getId(): string;
+
+    public function getRediSearchVersion(): string;
+
+    public function setRediSearchVersion(string $rediSearchVersion): self;
+
+    /**
+     * @return array<mixed>
+     */
+    public function getArguments(): array;
+
+    /**
+     * @param array<mixed>|float|int|mixed|string $data
+     *
+     * @return array<mixed>|int|Response|string
+     */
+    public function parseResponse($data);
+}
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index 497fba4..f3650d2 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -24,13 +24,13 @@
 use function count;
 use InvalidArgumentException;
 use function is_array;
+use MacFJA\RediSearch\Redis\Command;
 use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
-use Predis\Command\Command;
 
 /**
  * @SuppressWarnings(PHPMD.NumberOfChildren)
  */
-abstract class AbstractCommand extends Command
+abstract class AbstractCommand implements Command
 {
     public const MAX_IMPLEMENTED_VERSION = '2.0.12';
     public const MIN_IMPLEMENTED_VERSION = '2.0.0';
@@ -40,8 +40,6 @@ abstract class AbstractCommand extends Command
     protected $options;
     /** @var string */
     private $rediSearchVersion;
-    /** @var bool */
-    private $rebuild = true;
 
     /**
      * @param array<array<CommandOption>|CommandOption|mixed> $options
@@ -58,57 +56,13 @@ public function getRediSearchVersion(): string
         return $this->rediSearchVersion;
     }
 
-    public function setRediSearchVersion(string $rediSearchVersion): self
+    public function setRediSearchVersion(string $rediSearchVersion): Command
     {
         $this->rediSearchVersion = $rediSearchVersion;
 
         return $this;
     }
 
-    /**
-     * @codeCoverageIgnore
-     *
-     * @param mixed $index
-     */
-    public function getArgument($index)
-    {
-        if ($this->rebuild) {
-            $this->buildArguments();
-        }
-
-        return parent::getArgument($index);
-    }
-
-    /**
-     * @return array<mixed>
-     */
-    public function getArguments()
-    {
-        if ($this->rebuild) {
-            $this->buildArguments();
-        }
-
-        return parent::getArguments();
-    }
-
-    /**
-     * @param array<mixed> $arguments
-     */
-    public function setArguments(array $arguments): void
-    {
-        $this->rebuild = false;
-        parent::setArguments($arguments);
-    }
-
-    /**
-     * @param array<mixed> $arguments
-     */
-    public function setRawArguments(array $arguments): void
-    {
-        $this->rebuild = true;
-        parent::setRawArguments($arguments);
-    }
-
     /**
      * @param array<mixed>|float|int|mixed|string $data
      *
@@ -116,29 +70,13 @@ public function setRawArguments(array $arguments): void
      */
     public function parseResponse($data)
     {
-        if ($this->rebuild) {
-            return $this->transformParsedResponse(parent::parseResponse($data));
-        }
-
-        return parent::parseResponse($data);
+        return $this->transformParsedResponse($data);
     }
 
     /**
-     * @return array<string>
+     * @return array<float|int|string>
      */
-    abstract protected function getRequiredOptions(): array;
-
-    /**
-     * @param mixed $data
-     *
-     * @return mixed
-     */
-    protected function transformParsedResponse($data)
-    {
-        return $data;
-    }
-
-    private function buildArguments(): void
+    public function getArguments(): array
     {
         if (count($missing = $this->validateRequirements()) > 0) {
             throw new InvalidArgumentException('Missing command option: '.implode(', ', $missing));
@@ -155,10 +93,24 @@ private function buildArguments(): void
             return $option->isCompatible($this->rediSearchVersion) && $option->isValid();
         });
 
-        $arguments = array_reduce($arguments, function ($carry, CommandOption $option) {
+        return array_reduce($arguments, function ($carry, CommandOption $option) {
             return array_merge($carry, $option->render($this->rediSearchVersion));
         }, []);
-        $this->setRawArguments($arguments);
+    }
+
+    /**
+     * @return array<string>
+     */
+    abstract protected function getRequiredOptions(): array;
+
+    /**
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    protected function transformParsedResponse($data)
+    {
+        return $data;
     }
 
     /**
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 2a70626..5656581 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -160,7 +160,7 @@ public function setWithoutCursor(): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.AGGREGATE';
     }
diff --git a/src/Redis/Command/AliasAdd.php b/src/Redis/Command/AliasAdd.php
index fc7f58f..e20f63f 100644
--- a/src/Redis/Command/AliasAdd.php
+++ b/src/Redis/Command/AliasAdd.php
@@ -22,11 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class AliasAdd extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -51,7 +47,7 @@ public function setAlias(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.ALIASADD';
     }
diff --git a/src/Redis/Command/AliasDel.php b/src/Redis/Command/AliasDel.php
index 2c56470..2c58044 100644
--- a/src/Redis/Command/AliasDel.php
+++ b/src/Redis/Command/AliasDel.php
@@ -22,11 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class AliasDel extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -43,7 +39,7 @@ public function setAlias(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.ALIASDEL';
     }
diff --git a/src/Redis/Command/AliasUpdate.php b/src/Redis/Command/AliasUpdate.php
index ebdacac..e0dfdbf 100644
--- a/src/Redis/Command/AliasUpdate.php
+++ b/src/Redis/Command/AliasUpdate.php
@@ -22,11 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class AliasUpdate extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -51,7 +47,7 @@ public function setAlias(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.ALIASUPDATE';
     }
diff --git a/src/Redis/Command/Alter.php b/src/Redis/Command/Alter.php
index ab792d2..8d65c47 100644
--- a/src/Redis/Command/Alter.php
+++ b/src/Redis/Command/Alter.php
@@ -23,11 +23,7 @@
 
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class Alter extends AbstractCommand
 {
     use AddFieldOptionTrait;
@@ -49,7 +45,7 @@ public function setIndex(string $index): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.ALTER';
     }
diff --git a/src/Redis/Command/Config.php b/src/Redis/Command/Config.php
index dace6ed..ba449c6 100644
--- a/src/Redis/Command/Config.php
+++ b/src/Redis/Command/Config.php
@@ -53,7 +53,7 @@ public function setGetMode(?string $optionName = null): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.CONFIG';
     }
diff --git a/src/Redis/Command/ConfigSet.php b/src/Redis/Command/ConfigSet.php
index f4c3a4e..3702f0b 100644
--- a/src/Redis/Command/ConfigSet.php
+++ b/src/Redis/Command/ConfigSet.php
@@ -24,11 +24,7 @@
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class ConfigSet extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -57,7 +53,7 @@ public function setConfig(string $name, $value): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.CONFIG';
     }
diff --git a/src/Redis/Command/Create.php b/src/Redis/Command/Create.php
index 3fbb584..5687404 100644
--- a/src/Redis/Command/Create.php
+++ b/src/Redis/Command/Create.php
@@ -27,11 +27,7 @@
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
 use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class Create extends AbstractCommand
 {
     use LanguageOptionTrait;
@@ -190,7 +186,7 @@ public function setNoStopWords(): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.CREATE';
     }
diff --git a/src/Redis/Command/CursorDel.php b/src/Redis/Command/CursorDel.php
index 8edd956..897f25b 100644
--- a/src/Redis/Command/CursorDel.php
+++ b/src/Redis/Command/CursorDel.php
@@ -23,11 +23,7 @@
 
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class CursorDel extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -53,7 +49,7 @@ public function setCursorId(int $cursor): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.CURSOR';
     }
diff --git a/src/Redis/Command/CursorRead.php b/src/Redis/Command/CursorRead.php
index c289dd5..4c553ac 100644
--- a/src/Redis/Command/CursorRead.php
+++ b/src/Redis/Command/CursorRead.php
@@ -77,7 +77,7 @@ public function setNoCount(): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.CURSOR';
     }
diff --git a/src/Redis/Command/DictAdd.php b/src/Redis/Command/DictAdd.php
index 6639666..aa3b473 100644
--- a/src/Redis/Command/DictAdd.php
+++ b/src/Redis/Command/DictAdd.php
@@ -59,7 +59,7 @@ public function addTerms(string ...$term): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.DICTADD';
     }
diff --git a/src/Redis/Command/DictDel.php b/src/Redis/Command/DictDel.php
index 9d55062..4ae5875 100644
--- a/src/Redis/Command/DictDel.php
+++ b/src/Redis/Command/DictDel.php
@@ -59,7 +59,7 @@ public function setDictionary(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.DICTDEL';
     }
diff --git a/src/Redis/Command/DictDump.php b/src/Redis/Command/DictDump.php
index 126676e..3ad08c7 100644
--- a/src/Redis/Command/DictDump.php
+++ b/src/Redis/Command/DictDump.php
@@ -42,7 +42,7 @@ public function setDictionary(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.DICTDUMP';
     }
diff --git a/src/Redis/Command/DropIndex.php b/src/Redis/Command/DropIndex.php
index 61e03d6..37d5dbb 100644
--- a/src/Redis/Command/DropIndex.php
+++ b/src/Redis/Command/DropIndex.php
@@ -23,11 +23,7 @@
 
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
-use Predis\Response\Status;
 
-/**
- * @method Status parseResponse(mixed $data)
- */
 class DropIndex extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -52,7 +48,7 @@ public function setDeleteDocument(bool $active = true): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.DROPINDEX';
     }
diff --git a/src/Redis/Command/Explain.php b/src/Redis/Command/Explain.php
index f629718..a4c8358 100644
--- a/src/Redis/Command/Explain.php
+++ b/src/Redis/Command/Explain.php
@@ -50,7 +50,7 @@ public function setQuery(string $query): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.EXPLAIN';
     }
diff --git a/src/Redis/Command/ExplainCli.php b/src/Redis/Command/ExplainCli.php
index 0b303b5..334d5bd 100644
--- a/src/Redis/Command/ExplainCli.php
+++ b/src/Redis/Command/ExplainCli.php
@@ -26,7 +26,7 @@
  */
 class ExplainCli extends Explain
 {
-    public function getId()
+    public function getId(): string
     {
         return 'FT.EXPLAINCLI';
     }
diff --git a/src/Redis/Command/IndexList.php b/src/Redis/Command/IndexList.php
index d47b9aa..230c668 100644
--- a/src/Redis/Command/IndexList.php
+++ b/src/Redis/Command/IndexList.php
@@ -21,15 +21,40 @@
 
 namespace MacFJA\RediSearch\Redis\Command;
 
-use Predis\Command\Command;
+use MacFJA\RediSearch\Redis\Command;
 
-/**
- * @method array<string> parseResponse(mixed $data)
- */
-class IndexList extends Command
+class IndexList implements Command
 {
-    public function getId()
+    /** @var string */
+    private $rediSearchVersion = AbstractCommand::MIN_IMPLEMENTED_VERSION;
+
+    public function getId(): string
     {
         return 'FT._LIST';
     }
+
+    public function getRediSearchVersion(): string
+    {
+        return $this->rediSearchVersion;
+    }
+
+    /**
+     * @return IndexList
+     */
+    public function setRediSearchVersion(string $rediSearchVersion): Command
+    {
+        $this->rediSearchVersion = $rediSearchVersion;
+
+        return $this;
+    }
+
+    public function getArguments(): array
+    {
+        return [];
+    }
+
+    public function parseResponse($data)
+    {
+        return $data;
+    }
 }
diff --git a/src/Redis/Command/Info.php b/src/Redis/Command/Info.php
index 8270c60..8c0eb8e 100644
--- a/src/Redis/Command/Info.php
+++ b/src/Redis/Command/Info.php
@@ -43,7 +43,7 @@ public function setIndex(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.INFO';
     }
diff --git a/src/Redis/Command/PaginatedCommand.php b/src/Redis/Command/PaginatedCommand.php
index e52746f..96a2183 100644
--- a/src/Redis/Command/PaginatedCommand.php
+++ b/src/Redis/Command/PaginatedCommand.php
@@ -21,9 +21,9 @@
 
 namespace MacFJA\RediSearch\Redis\Command;
 
-use Predis\Command\CommandInterface;
+use MacFJA\RediSearch\Redis\Command;
 
-interface PaginatedCommand extends CommandInterface
+interface PaginatedCommand extends Command
 {
     public function getOffset(): ?int;
 
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index cdb65c7..5dba9b3 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -279,7 +279,7 @@ public function setReturn(string ...$field): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SEARCH';
     }
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
index 565f0dd..06d1850 100644
--- a/src/Redis/Command/SpellCheck.php
+++ b/src/Redis/Command/SpellCheck.php
@@ -78,7 +78,7 @@ public function addTerms(string $dictionary, bool $isExcluding = false): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SPELLCHECK';
     }
diff --git a/src/Redis/Command/SugAdd.php b/src/Redis/Command/SugAdd.php
index 2b84143..2b8ea4c 100644
--- a/src/Redis/Command/SugAdd.php
+++ b/src/Redis/Command/SugAdd.php
@@ -79,7 +79,7 @@ public function setPayload(string $payload): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SUGADD';
     }
diff --git a/src/Redis/Command/SugDel.php b/src/Redis/Command/SugDel.php
index 466ea98..a58ee11 100644
--- a/src/Redis/Command/SugDel.php
+++ b/src/Redis/Command/SugDel.php
@@ -50,7 +50,7 @@ public function setSuggestion(string $value): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SUGDEL';
     }
diff --git a/src/Redis/Command/SugGet.php b/src/Redis/Command/SugGet.php
index 6bba1a0..fd0a71d 100644
--- a/src/Redis/Command/SugGet.php
+++ b/src/Redis/Command/SugGet.php
@@ -92,7 +92,7 @@ public function setMax(int $max): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SUGGET';
     }
diff --git a/src/Redis/Command/SugLen.php b/src/Redis/Command/SugLen.php
index 6247820..9e664dc 100644
--- a/src/Redis/Command/SugLen.php
+++ b/src/Redis/Command/SugLen.php
@@ -42,7 +42,7 @@ public function setDictionary(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SUGLEN';
     }
diff --git a/src/Redis/Command/SynDump.php b/src/Redis/Command/SynDump.php
index 36190bf..87aede2 100644
--- a/src/Redis/Command/SynDump.php
+++ b/src/Redis/Command/SynDump.php
@@ -39,7 +39,7 @@ public function setIndex(string $index): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SYNDUMP';
     }
diff --git a/src/Redis/Command/SynUpdate.php b/src/Redis/Command/SynUpdate.php
index a581c16..10f37f3 100644
--- a/src/Redis/Command/SynUpdate.php
+++ b/src/Redis/Command/SynUpdate.php
@@ -66,7 +66,7 @@ public function setTerms(string ...$term): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.SYNUPDATE';
     }
diff --git a/src/Redis/Command/TagVals.php b/src/Redis/Command/TagVals.php
index 5024d43..86a0d56 100644
--- a/src/Redis/Command/TagVals.php
+++ b/src/Redis/Command/TagVals.php
@@ -50,7 +50,7 @@ public function setField(string $name): self
         return $this;
     }
 
-    public function getId()
+    public function getId(): string
     {
         return 'FT.TAGVALS';
     }
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 78e0e51..4993aa0 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -48,7 +48,6 @@
 use MacFJA\RediSearch\Redis\Command\SynDump;
 use MacFJA\RediSearch\Redis\Command\SynUpdate;
 use MacFJA\RediSearch\Redis\Command\TagVals;
-use Predis\ClientInterface;
 use Predis\Profile\RedisProfile;
 
 /**
@@ -90,9 +89,9 @@ public static function registerCommands(RedisProfile $profile): void
         $profile->defineCommand('fttagvals', TagVals::class);
     }
 
-    public static function getRediSearchVersion(ClientInterface $client): ?string
+    public static function getRediSearchVersion(Client $client): ?string
     {
-        $modules = $client->info('Modules')['Modules'] ?? [];
+        $modules = $client->executeRaw('info', 'Modules')['Modules'] ?? [];
 
         foreach ($modules as $module) {
             $data = array_column(
diff --git a/src/Redis/Response.php b/src/Redis/Response.php
new file mode 100644
index 0000000..a4b7a5c
--- /dev/null
+++ b/src/Redis/Response.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis;
+
+interface Response
+{
+}
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index 1ad8fa2..dd1d9a4 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -23,21 +23,21 @@
 
 use function count;
 use Iterator;
+use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\CursorRead;
-use Predis\ClientInterface;
-use Predis\Response\ResponseInterface;
+use MacFJA\RediSearch\Redis\Response;
 
 /**
  * @implements Iterator<int,AggregateResponseItem[]>
  */
-class CursorResponse implements ResponseInterface, Iterator
+class CursorResponse implements Response, Iterator
 {
     /** @var array<AggregateResponseItem> */
     private $items;
     /** @var int */
     private $totalCount;
-    /** @var ClientInterface */
+    /** @var Client */
     private $client;
     /** @var bool */
     private $doNext = false;
@@ -65,7 +65,7 @@ public function __construct(int $cursorId, int $totalCount, array $items, int $s
         $this->cursorId = $cursorId;
     }
 
-    public function setClient(ClientInterface $client): self
+    public function setClient(Client $client): self
     {
         $this->client = $client;
 
@@ -83,7 +83,7 @@ public function current()
             $cursorRead->setCursorId($this->cursorId);
             $cursorRead->setCount($this->getPageSize());
             /** @var CursorResponse $next */
-            $next = $this->client->executeCommand($cursorRead);
+            $next = $this->client->execute($cursorRead);
             $this->cursorId = $next->cursorId;
             $this->offset += count($this->items);
             $this->items = $next->items;
diff --git a/src/Redis/Response/InfoResponse.php b/src/Redis/Response/InfoResponse.php
index 2cd23df..3d7a7ac 100644
--- a/src/Redis/Response/InfoResponse.php
+++ b/src/Redis/Response/InfoResponse.php
@@ -26,6 +26,7 @@
 use function in_array;
 use InvalidArgumentException;
 use function is_array;
+use function is_object;
 use function is_string;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
@@ -33,8 +34,8 @@
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
-use Predis\Response\ResponseInterface;
-use Predis\Response\Status;
+use MacFJA\RediSearch\Redis\Response;
+use Stringable;
 
 /**
  * @method string     getIndexName()
@@ -61,7 +62,7 @@
  * @method array      getCursorStats()
  * @method array      getStopwordsList()
  */
-class InfoResponse implements ResponseInterface
+class InfoResponse implements Response
 {
     use ArrayResponseTrait;
 
@@ -71,12 +72,12 @@ class InfoResponse implements ResponseInterface
     private const TO_BOOLEAN = ['indexing'];
 
     /**
-     * @var array<array<mixed>|float|int|Status|string>
+     * @var array<array<mixed>|float|int|string>
      */
     private $rawLines;
 
     /**
-     * @param array<array<mixed>|float|int|Status|string> $rawLines
+     * @param array<array<mixed>|float|int|string> $rawLines
      */
     public function __construct($rawLines)
     {
@@ -122,8 +123,8 @@ public function getFieldsAsOption(): array
         return array_map(static function (array $field): CreateCommandFieldOption {
             $fieldName = (string) array_shift($field);
             array_walk($field, static function (&$item) {
-                if ($item instanceof Status) {
-                    $item = $item->getPayload();
+                if (is_object($item) && ($item instanceof Stringable || method_exists($item, '__toString'))) {
+                    $item = (string) $item;
                 }
 
                 return $item;
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index a787326..222e576 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -24,14 +24,14 @@
 use function count;
 use function is_int;
 use Iterator;
+use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\PaginatedCommand;
-use Predis\ClientInterface;
-use Predis\Response\ResponseInterface;
+use MacFJA\RediSearch\Redis\Response;
 
 /**
  * @implements Iterator<int,AggregateResponseItem[]|SearchResponseItem[]>
  */
-class PaginatedResponse implements ResponseInterface, Iterator
+class PaginatedResponse implements Response, Iterator
 {
     /** @var array<AggregateResponseItem>|array<SearchResponseItem> */
     private $items;
@@ -39,7 +39,7 @@ class PaginatedResponse implements ResponseInterface, Iterator
     private $lastCommand;
     /** @var int */
     private $totalCount;
-    /** @var ClientInterface */
+    /** @var Client */
     private $client;
 
     /** @var null|int */
@@ -57,7 +57,7 @@ public function __construct(PaginatedCommand $command, int $totalCount, array $i
         $this->lastCommand = $command;
     }
 
-    public function setClient(ClientInterface $client): PaginatedResponse
+    public function setClient(Client $client): PaginatedResponse
     {
         $this->client = $client;
 
@@ -132,7 +132,7 @@ private function updateWithLimit(int $offset, int $size): void
         $nextCommand->setLimit($offset, $size);
 
         /** @var PaginatedResponse $paginated */
-        $paginated = $this->client->executeCommand($nextCommand);
+        $paginated = $this->client->execute($nextCommand);
         $this->lastCommand = $nextCommand;
         $this->totalCount = $paginated->totalCount;
         $this->items = $paginated->items;
diff --git a/src/Suggestions.php b/src/Suggestions.php
index 52649b3..ad785ec 100644
--- a/src/Suggestions.php
+++ b/src/Suggestions.php
@@ -23,6 +23,7 @@
 
 use function is_int;
 use function is_string;
+use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\SugAdd;
 use MacFJA\RediSearch\Redis\Command\SugDel;
@@ -30,7 +31,6 @@
 use MacFJA\RediSearch\Redis\Command\SugLen;
 use MacFJA\RediSearch\Redis\Initializer;
 use MacFJA\RediSearch\Redis\Response\SuggestionResponseItem;
-use Predis\ClientInterface;
 
 /**
  * @codeCoverageIgnore
@@ -39,18 +39,18 @@ class Suggestions
 {
     /** @var string */
     private $dictionary;
-    /** @var ClientInterface */
+    /** @var Client */
     private $client;
     /** @var int */
     private $length;
     /** @var string */
     private $version;
 
-    public function __construct(string $dictionary, ClientInterface $client)
+    public function __construct(string $dictionary, Client $client)
     {
         $this->dictionary = $dictionary;
         $this->client = $client;
-        $this->length = $client->executeCommand((new SugLen())->setDictionary($dictionary));
+        $this->length = (int) $client->execute((new SugLen())->setDictionary($dictionary));
         $this->version = Initializer::getRediSearchVersion($client) ?? AbstractCommand::MIN_IMPLEMENTED_VERSION;
     }
 
@@ -67,7 +67,7 @@ public function add(string $suggestion, float $score, bool $increment = false, ?
             $command->setPayload($payload);
         }
 
-        $response = $this->client->executeCommand($command);
+        $response = $this->client->execute($command);
 
         if (is_int($response)) {
             $this->length = $response;
@@ -90,7 +90,7 @@ public function get(string $prefix, bool $fuzzy = false, bool $withScores = fals
             $command->setMax($max);
         }
 
-        return $this->client->executeCommand($command);
+        return $this->client->execute($command) ?? [];
     }
 
     public function delete(string $suggestion): bool
@@ -100,7 +100,7 @@ public function delete(string $suggestion): bool
             ->setDictionary($this->dictionary)
             ->setSuggestion($suggestion)
         ;
-        $removed = (int) $this->client->executeCommand($command);
+        $removed = (int) $this->client->execute($command);
         $this->length -= $removed;
 
         return 1 === $removed;
diff --git a/tests/Redis/InitializerTest.php b/tests/Redis/InitializerTest.php
index 40bb653..03e2c66 100644
--- a/tests/Redis/InitializerTest.php
+++ b/tests/Redis/InitializerTest.php
@@ -22,8 +22,8 @@
 namespace MacFJA\RediSearch\tests\Redis;
 
 use Generator;
+use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Initializer;
-use Predis\Client;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Initializer
@@ -38,11 +38,8 @@ class InitializerTest extends \PHPUnit\Framework\TestCase
      */
     public function testRediSearchVersion(array $input, ?string $expected): void
     {
-        $client = $this->getMockBuilder(Client::class)
-            ->addMethods(['info'])
-            ->getMock()
-        ;
-        $client->method('info')->with('Modules')->willReturn(['Modules' => $input]);
+        $client = $this->createMock(Client::class);
+        $client->method('executeRaw')->with('info', 'Modules')->willReturn(['Modules' => $input]);
 
         static::assertSame($expected, Initializer::getRediSearchVersion($client));
     }
diff --git a/tests/fixtures/Redis/Command/FakeAbstractCommand.php b/tests/fixtures/Redis/Command/FakeAbstractCommand.php
index 9f184c9..9dbe16f 100644
--- a/tests/fixtures/Redis/Command/FakeAbstractCommand.php
+++ b/tests/fixtures/Redis/Command/FakeAbstractCommand.php
@@ -29,7 +29,7 @@ class FakeAbstractCommand extends \MacFJA\RediSearch\Redis\Command\AbstractComma
     /** @var array<string> */
     private $requiredOptions;
 
-    public function getId()
+    public function getId(): string
     {
         return '';
     }
diff --git a/tests/psalm/stubs/phpiredis.php b/tests/psalm/stubs/phpiredis.php
new file mode 100644
index 0000000..57192c9
--- /dev/null
+++ b/tests/psalm/stubs/phpiredis.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+const PHPIREDIS_READER_STATE_COMPLETE = 1;
+const PHPIREDIS_READER_STATE_INCOMPLETE = 2;
+const PHPIREDIS_READER_STATE_ERROR = 3;
+
+const PHPIREDIS_REPLY_STRING = 1;
+const PHPIREDIS_REPLY_ARRAY = 2;
+const PHPIREDIS_REPLY_INTEGER = 3;
+const PHPIREDIS_REPLY_NIL = 4;
+const PHPIREDIS_REPLY_STATUS = 5;
+const PHPIREDIS_REPLY_ERROR = 6;
+
+function phpiredis_connect($host, $port = 6379): void
+{
+}
+function phpiredis_pconnect($host, $port = 6379): void
+{
+}
+function phpiredis_disconnect(): void
+{
+}
+/**
+ * @param resource $redis
+ * @param string[] $command
+ *
+ * @return bool|string
+ */
+function phpiredis_command_bs($redis, array $command)
+{
+}
+
+/**
+ * @param resource $redis
+ *
+ * @return bool|string
+ */
+function phpiredis_command($redis, string $command)
+{
+}
+/**
+ * @param resource      $redis
+ * @param array<string> $commands
+ *
+ * @return array<bool|string>
+ */
+function phpiredis_multi_command($redis, array $commands)
+{
+}
+
+/**
+ * @param resource             $redis
+ * @param array<array<string>> $commands
+ *
+ * @return array<bool|string>
+ */
+function phpiredis_multi_command_bs($redis, array $commands)
+{
+}
+
+/**
+ * @param array<float|int|string> $args
+ */
+function phpiredis_format_command(array $args): string
+{
+}
+
+/**
+ * @return resource
+ */
+function phpiredis_reader_create()
+{
+}
+
+/**
+ * @param resource $reader
+ */
+function phpiredis_reader_reset($reader): void
+{
+}
+
+/**
+ * @param resource $reader
+ */
+function phpiredis_reader_feed($reader, string $response): void
+{
+}
+
+/**
+ * @param resource $reader
+ *
+ * @return bool|int
+ */
+function phpiredis_reader_get_state($reader)
+{
+}
+/**
+ * @param resource $reader
+ *
+ * @return bool|string
+ */
+function phpiredis_reader_get_error($reader)
+{
+}
+
+/**
+ * @param resource $reader
+ *
+ * @return array<array|bool|int|string>|bool|int|string
+ */
+function phpiredis_reader_get_reply($reader, int &$type)
+{
+}
+
+/**
+ * @param resource $redis
+ */
+function phpiredis_reader_destroy($redis): void
+{
+}
+
+/**
+ * @param resource $reader
+ */
+function phpiredis_reader_set_error_handler($reader, ?callable $handler): void
+{
+}
+/**
+ * @param resource $reader
+ */
+function phpiredis_reader_set_status_handler($reader, ?callable $handler): void
+{
+}
+function phpiredis_utils_crc16(string $data): int
+{
+}

From a68121649af3aa1f9c8b7fb814a491fc316dec16 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 17 Oct 2021 03:25:28 +0200
Subject: [PATCH 14/60] Add Amp, Cheprasov, Credis, Redisent, Rediska client +
 refactoring

---
 composer.json                             |  12 +-
 composer.lock                             | 959 +++++++++++++++++++++-
 src/Redis/Client/AbstractClient.php       |  96 +++
 src/Redis/Client/AmpRedisClient.php       |  88 ++
 src/Redis/Client/CheprasovRedisClient.php |  89 ++
 src/Redis/Client/ClientFacade.php         |  22 +-
 src/Redis/Client/CredisClient.php         |  83 ++
 src/Redis/Client/PhpiredisClient.php      |  32 +-
 src/Redis/Client/PhpredisClient.php       |  23 +-
 src/Redis/Client/PredisClient.php         |  40 +-
 src/Redis/Client/RedisentClient.php       |  83 ++
 src/Redis/Client/RediskaClient.php        | 102 +++
 src/Redis/Initializer.php                 |  74 +-
 src/Redis/Response/InfoResponse.php       |   4 +-
 tests/Redis/InitializerTest.php           |  16 +-
 15 files changed, 1635 insertions(+), 88 deletions(-)
 create mode 100644 src/Redis/Client/AbstractClient.php
 create mode 100644 src/Redis/Client/AmpRedisClient.php
 create mode 100644 src/Redis/Client/CheprasovRedisClient.php
 create mode 100644 src/Redis/Client/CredisClient.php
 create mode 100644 src/Redis/Client/RedisentClient.php
 create mode 100644 src/Redis/Client/RediskaClient.php

diff --git a/composer.json b/composer.json
index b8a3d4e..6cc14a3 100644
--- a/composer.json
+++ b/composer.json
@@ -22,15 +22,20 @@
         "respect/validation": "^2.0"
     },
     "require-dev": {
+        "amphp/redis": "^1.0",
+        "cheprasov/php-redis-client": "^1.10",
+        "colinmollenhour/credis": "^1.12",
         "enlightn/security-checker": "^1.9",
         "ergebnis/composer-normalize": "^2.13",
         "friendsofphp/php-cs-fixer": "^3.0",
+        "geometria-lab/rediska": "^0.5.10",
         "insolita/unused-scanner": "^2.3",
         "php-parallel-lint/php-parallel-lint": "^1.3",
         "phpmd/phpmd": "^2.10",
         "phpstan/phpstan": "^0.12.92",
         "phpunit/phpunit": "^8.5",
         "predis/predis": "^1.1",
+        "redisent/redisent": "dev-master",
         "roave/security-advisories": "dev-latest",
         "rskuipers/php-assumptions": "^0.8.0",
         "sebastian/phpcpd": "^4.1",
@@ -40,7 +45,12 @@
     "suggest": {
         "ext-phpiredis": "To use Phpiredis extension implementation",
         "ext-phpredis": "To use Phpredis extension implementation",
-        "predis/predis": "To use Predis implementation"
+        "amphp/redis": "To use AmpPhp implementation",
+        "cheprasov/php-redis-client": "To use Cheprasov implementation",
+        "colinmollenhour/credis": "To use Credis implementation",
+        "geometria-lab/rediska": "To use Rediska implementation",
+        "predis/predis": "To use Predis implementation",
+        "redisent/redisent": "To use Redisent implementation"
     },
     "config": {
         "platform": {
diff --git a/composer.lock b/composer.lock
index 6075858..acdc1c0 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "d631916a7eaceb593add53d973c6cb88",
+    "content-hash": "6fe468a3d47a4506b963d1d03e7c38af",
     "packages": [
         {
             "name": "composer/semver",
@@ -458,6 +458,690 @@
             ],
             "time": "2021-03-30T17:13:30+00:00"
         },
+        {
+            "name": "amphp/cache",
+            "version": "v1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/cache.git",
+                "reference": "2b6b5dbb70e54cc914df9952ba7c012bc4cbcd28"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/cache/zipball/2b6b5dbb70e54cc914df9952ba7c012bc4cbcd28",
+                "reference": "2b6b5dbb70e54cc914df9952ba7c012bc4cbcd28",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2",
+                "amphp/serialization": "^1",
+                "amphp/sync": "^1.2",
+                "php": ">=7.1"
+            },
+            "conflict": {
+                "amphp/file": "<0.2 || >=3"
+            },
+            "require-dev": {
+                "amphp/file": "^1 || ^2",
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1.1",
+                "phpunit/phpunit": "^6 | ^7 | ^8 | ^9",
+                "vimeo/psalm": "^4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Cache\\": "lib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                },
+                {
+                    "name": "Daniel Lowrey",
+                    "email": "rdlowrey@php.net"
+                }
+            ],
+            "description": "A promise-aware caching API for Amp.",
+            "homepage": "https://github.com/amphp/cache",
+            "support": {
+                "irc": "irc://irc.freenode.org/amphp",
+                "issues": "https://github.com/amphp/cache/issues",
+                "source": "https://github.com/amphp/cache/tree/v1.5.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-06-29T17:12:43+00:00"
+        },
+        {
+            "name": "amphp/dns",
+            "version": "v1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/dns.git",
+                "reference": "852292532294d7972c729a96b49756d781f7c59d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/dns/zipball/852292532294d7972c729a96b49756d781f7c59d",
+                "reference": "852292532294d7972c729a96b49756d781f7c59d",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2",
+                "amphp/byte-stream": "^1.1",
+                "amphp/cache": "^1.2",
+                "amphp/parser": "^1",
+                "amphp/windows-registry": "^0.3",
+                "daverandom/libdns": "^2.0.1",
+                "ext-filter": "*",
+                "ext-json": "*",
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1",
+                "phpunit/phpunit": "^6 || ^7 || ^8 || ^9"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Dns\\": "lib"
+                },
+                "files": [
+                    "lib/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Chris Wright",
+                    "email": "addr@daverandom.com"
+                },
+                {
+                    "name": "Daniel Lowrey",
+                    "email": "rdlowrey@php.net"
+                },
+                {
+                    "name": "Bob Weinand",
+                    "email": "bobwei9@hotmail.com"
+                },
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                },
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                }
+            ],
+            "description": "Async DNS resolution for Amp.",
+            "homepage": "https://github.com/amphp/dns",
+            "keywords": [
+                "amp",
+                "amphp",
+                "async",
+                "client",
+                "dns",
+                "resolve"
+            ],
+            "support": {
+                "issues": "https://github.com/amphp/dns/issues",
+                "source": "https://github.com/amphp/dns/tree/v1.2.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-07-21T19:04:57+00:00"
+        },
+        {
+            "name": "amphp/parser",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/parser.git",
+                "reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/parser/zipball/f83e68f03d5b8e8e0365b8792985a7f341c57ae1",
+                "reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.3",
+                "phpunit/phpunit": "^6"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Parser\\": "lib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                },
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                }
+            ],
+            "description": "A generator parser to make streaming parsers simple.",
+            "homepage": "https://github.com/amphp/parser",
+            "keywords": [
+                "async",
+                "non-blocking",
+                "parser",
+                "stream"
+            ],
+            "support": {
+                "issues": "https://github.com/amphp/parser/issues",
+                "source": "https://github.com/amphp/parser/tree/is-valid"
+            },
+            "time": "2017-06-06T05:29:10+00:00"
+        },
+        {
+            "name": "amphp/process",
+            "version": "v1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/process.git",
+                "reference": "3d36327bf9b4c158cf3010f8f65c00854ec3a8b7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/process/zipball/3d36327bf9b4c158cf3010f8f65c00854ec3a8b7",
+                "reference": "3d36327bf9b4c158cf3010f8f65c00854ec3a8b7",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2",
+                "amphp/byte-stream": "^1.4",
+                "php": ">=7"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1",
+                "phpunit/phpunit": "^6"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Process\\": "lib"
+                },
+                "files": [
+                    "lib/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bob Weinand",
+                    "email": "bobwei9@hotmail.com"
+                },
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                },
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "Asynchronous process manager.",
+            "homepage": "https://github.com/amphp/process",
+            "support": {
+                "issues": "https://github.com/amphp/process/issues",
+                "source": "https://github.com/amphp/process/tree/v1.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-10-08T15:55:53+00:00"
+        },
+        {
+            "name": "amphp/redis",
+            "version": "v1.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/redis.git",
+                "reference": "b8910b1051b128105f0724b5c95c34db30bd1d45"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/redis/zipball/b8910b1051b128105f0724b5c95c34db30bd1d45",
+                "reference": "b8910b1051b128105f0724b5c95c34db30bd1d45",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2.2.1",
+                "amphp/cache": "^1.2.1",
+                "amphp/socket": "^1",
+                "amphp/sync": "^1.1",
+                "league/uri-parser": "^1.4",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1.1.2",
+                "phpunit/phpunit": "^7 | ^8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Redis\\": "src"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "A non-blocking Redis client for Amp.",
+            "homepage": "https://github.com/amphp/redis",
+            "keywords": [
+                "amp",
+                "amphp",
+                "async",
+                "client",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/amphp/redis/issues",
+                "source": "https://github.com/amphp/redis/tree/v1.0.5"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-10-12T18:38:52+00:00"
+        },
+        {
+            "name": "amphp/serialization",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/serialization.git",
+                "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1",
+                "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "phpunit/phpunit": "^9 || ^8 || ^7"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Serialization\\": "src"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                },
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "Serialization tools for IPC and data storage in PHP.",
+            "homepage": "https://github.com/amphp/serialization",
+            "keywords": [
+                "async",
+                "asynchronous",
+                "serialization",
+                "serialize"
+            ],
+            "support": {
+                "issues": "https://github.com/amphp/serialization/issues",
+                "source": "https://github.com/amphp/serialization/tree/master"
+            },
+            "time": "2020-03-25T21:39:07+00:00"
+        },
+        {
+            "name": "amphp/socket",
+            "version": "v1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/socket.git",
+                "reference": "a8af9f5d0a66c5fe9567da45a51509e592788fe6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/socket/zipball/a8af9f5d0a66c5fe9567da45a51509e592788fe6",
+                "reference": "a8af9f5d0a66c5fe9567da45a51509e592788fe6",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2",
+                "amphp/byte-stream": "^1.6",
+                "amphp/dns": "^1 || ^0.9",
+                "ext-openssl": "*",
+                "kelunik/certificate": "^1.1",
+                "league/uri-parser": "^1.4",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1",
+                "phpunit/phpunit": "^6 || ^7 || ^8",
+                "vimeo/psalm": "^3.9@dev"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Socket\\": "src"
+                },
+                "files": [
+                    "src/functions.php",
+                    "src/Internal/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniel Lowrey",
+                    "email": "rdlowrey@gmail.com"
+                },
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                },
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "Async socket connection / server tools for Amp.",
+            "homepage": "https://github.com/amphp/socket",
+            "keywords": [
+                "amp",
+                "async",
+                "encryption",
+                "non-blocking",
+                "sockets",
+                "tcp",
+                "tls"
+            ],
+            "support": {
+                "issues": "https://github.com/amphp/socket/issues",
+                "source": "https://github.com/amphp/socket/tree/v1.2.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-07-09T18:18:48+00:00"
+        },
+        {
+            "name": "amphp/sync",
+            "version": "v1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/sync.git",
+                "reference": "613047ac54c025aa800a9cde5b05c3add7327ed4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/sync/zipball/613047ac54c025aa800a9cde5b05c3add7327ed4",
+                "reference": "613047ac54c025aa800a9cde5b05c3add7327ed4",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2.2",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master",
+                "amphp/phpunit-util": "^1.1",
+                "phpunit/phpunit": "^9 || ^8 || ^7"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\Sync\\": "src"
+                },
+                "files": [
+                    "src/functions.php",
+                    "src/ConcurrentIterator/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Aaron Piotrowski",
+                    "email": "aaron@trowski.com"
+                },
+                {
+                    "name": "Stephen Coakley",
+                    "email": "me@stephencoakley.com"
+                }
+            ],
+            "description": "Mutex, Semaphore, and other synchronization tools for Amp.",
+            "homepage": "https://github.com/amphp/sync",
+            "keywords": [
+                "async",
+                "asynchronous",
+                "mutex",
+                "semaphore",
+                "synchronization"
+            ],
+            "support": {
+                "issues": "https://github.com/amphp/sync/issues",
+                "source": "https://github.com/amphp/sync/tree/v1.4.0"
+            },
+            "time": "2020-05-07T18:57:50+00:00"
+        },
+        {
+            "name": "amphp/windows-registry",
+            "version": "v0.3.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/amphp/windows-registry.git",
+                "reference": "0f56438b9197e224325e88f305346f0221df1f71"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/amphp/windows-registry/zipball/0f56438b9197e224325e88f305346f0221df1f71",
+                "reference": "0f56438b9197e224325e88f305346f0221df1f71",
+                "shasum": ""
+            },
+            "require": {
+                "amphp/amp": "^2",
+                "amphp/byte-stream": "^1.4",
+                "amphp/process": "^1"
+            },
+            "require-dev": {
+                "amphp/php-cs-fixer-config": "dev-master"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Amp\\WindowsRegistry\\": "lib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "Windows Registry Reader.",
+            "support": {
+                "issues": "https://github.com/amphp/windows-registry/issues",
+                "source": "https://github.com/amphp/windows-registry/tree/master"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/amphp",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-07-10T16:13:29+00:00"
+        },
+        {
+            "name": "cheprasov/php-redis-client",
+            "version": "1.10.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/cheprasov/php-redis-client.git",
+                "reference": "c3594e0bbc8ee1bc2c11c889ce5dfa0d3b8a6f9a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/cheprasov/php-redis-client/zipball/c3594e0bbc8ee1bc2c11c889ce5dfa0d3b8a6f9a",
+                "reference": "c3594e0bbc8ee1bc2c11c889ce5dfa0d3b8a6f9a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "cheprasov/php-extra-mocks": "^1.0.0",
+                "phpunit/phpunit": "4.8.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "RedisClient\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Alexander Cheprasov",
+                    "email": "acheprasov84@gmail.com"
+                }
+            ],
+            "description": "Php client for Redis. It is a fast, fully-functional and user-friendly client for Redis, optimized for performance. RedisClient supports the latest versions of Redis starting from 2.6 to 6.0",
+            "homepage": "http://github.com/cheprasov/php-redis-client",
+            "support": {
+                "issues": "https://github.com/cheprasov/php-redis-client/issues",
+                "source": "https://github.com/cheprasov/php-redis-client/tree/v1.10.0"
+            },
+            "time": "2020-06-20T13:05:24+00:00"
+        },
+        {
+            "name": "colinmollenhour/credis",
+            "version": "v1.12.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/colinmollenhour/credis.git",
+                "reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/colinmollenhour/credis/zipball/c27faa11724229986335c23f4b6d0f1d8d6547fb",
+                "reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "Client.php",
+                    "Cluster.php",
+                    "Sentinel.php",
+                    "Module.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Colin Mollenhour",
+                    "email": "colin@mollenhour.com"
+                }
+            ],
+            "description": "Credis is a lightweight interface to the Redis key-value store which wraps the phpredis library when available for better performance.",
+            "homepage": "https://github.com/colinmollenhour/credis",
+            "support": {
+                "issues": "https://github.com/colinmollenhour/credis/issues",
+                "source": "https://github.com/colinmollenhour/credis/tree/v1.12.1"
+            },
+            "time": "2020-11-06T16:09:14+00:00"
+        },
         {
             "name": "composer/package-versions-deprecated",
             "version": "1.11.99.4",
@@ -595,6 +1279,50 @@
             ],
             "time": "2021-07-31T17:03:58+00:00"
         },
+        {
+            "name": "daverandom/libdns",
+            "version": "v2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/DaveRandom/LibDNS.git",
+                "reference": "e8b6d6593d18ac3a6a14666d8a68a4703b2e05f9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/e8b6d6593d18ac3a6a14666d8a68a4703b2e05f9",
+                "reference": "e8b6d6593d18ac3a6a14666d8a68a4703b2e05f9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "php": ">=7.0"
+            },
+            "suggest": {
+                "ext-intl": "Required for IDN support"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "LibDNS\\": "src/"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "DNS protocol implementation written in pure PHP",
+            "keywords": [
+                "dns"
+            ],
+            "support": {
+                "issues": "https://github.com/DaveRandom/LibDNS/issues",
+                "source": "https://github.com/DaveRandom/LibDNS/tree/v2.0.2"
+            },
+            "time": "2019-12-03T09:12:46+00:00"
+        },
         {
             "name": "dnoegel/php-xdg-base-dir",
             "version": "v0.1.1",
@@ -1329,6 +2057,65 @@
             ],
             "time": "2021-08-29T20:16:20+00:00"
         },
+        {
+            "name": "geometria-lab/rediska",
+            "version": "v0.5.10",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Shumkov/Rediska.git",
+                "reference": "5b33ba2f1526f9ad30864b42617e34ac370aeae0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Shumkov/Rediska/zipball/5b33ba2f1526f9ad30864b42617e34ac370aeae0",
+                "reference": "5b33ba2f1526f9ad30864b42617e34ac370aeae0",
+                "shasum": ""
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=3.7.11",
+                "zendframework/zendframework1": ">=1.12.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Rediska": [
+                        "library/",
+                        "tests/library/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "authors": [
+                {
+                    "name": "Maxim Ivanov",
+                    "email": "maximiv@gmail.com"
+                },
+                {
+                    "name": "Ryan Grenz",
+                    "email": "info@ryangrenz.com"
+                },
+                {
+                    "name": "Ivan Shumkov",
+                    "email": "ivan@shumkov.ru"
+                },
+                {
+                    "name": "Till Klampaeckel",
+                    "email": "till@php.net"
+                }
+            ],
+            "description": "Full-featured PHP client for key-value database Redis",
+            "keywords": [
+                "cache",
+                "redis",
+                "sessions",
+                "zend"
+            ],
+            "support": {
+                "issues": "https://github.com/Shumkov/Rediska/issues",
+                "source": "https://github.com/Shumkov/Rediska/tree/v0.5.10"
+            },
+            "time": "2014-08-12T15:02:35+00:00"
+        },
         {
             "name": "guzzlehttp/guzzle",
             "version": "6.5.5",
@@ -1658,6 +2445,59 @@
             },
             "time": "2021-07-22T09:24:00+00:00"
         },
+        {
+            "name": "kelunik/certificate",
+            "version": "v1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/kelunik/certificate.git",
+                "reference": "56542e62d51533d04d0a9713261fea546bff80f6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/kelunik/certificate/zipball/56542e62d51533d04d0a9713261fea546bff80f6",
+                "reference": "56542e62d51533d04d0a9713261fea546bff80f6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-openssl": "*",
+                "php": ">=5.4"
+            },
+            "require-dev": {
+                "fabpot/php-cs-fixer": "^1.9",
+                "phpunit/phpunit": "^4.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Kelunik\\Certificate\\": "lib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Niklas Keller",
+                    "email": "me@kelunik.com"
+                }
+            ],
+            "description": "Access certificate details and transform between different formats.",
+            "keywords": [
+                "DER",
+                "certificate",
+                "certificates",
+                "openssl",
+                "pem",
+                "x509"
+            ],
+            "support": {
+                "issues": "https://github.com/kelunik/certificate/issues",
+                "source": "https://github.com/kelunik/certificate/tree/v1.1.2"
+            },
+            "time": "2019-05-29T19:02:31+00:00"
+        },
         {
             "name": "league/climate",
             "version": "3.6.0",
@@ -1723,6 +2563,75 @@
             },
             "time": "2020-10-04T16:02:59+00:00"
         },
+        {
+            "name": "league/uri-parser",
+            "version": "1.4.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/uri-parser.git",
+                "reference": "671548427e4c932352d9b9279fdfa345bf63fa00"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/uri-parser/zipball/671548427e4c932352d9b9279fdfa345bf63fa00",
+                "reference": "671548427e4c932352d9b9279fdfa345bf63fa00",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.0",
+                "phpstan/phpstan": "^0.9.2",
+                "phpstan/phpstan-phpunit": "^0.9.4",
+                "phpstan/phpstan-strict-rules": "^0.9.0",
+                "phpunit/phpunit": "^6.0"
+            },
+            "suggest": {
+                "ext-intl": "Allow parsing RFC3987 compliant hosts",
+                "league/uri-schemes": "Allow validating and normalizing URI parsing results"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "League\\Uri\\": "src"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ignace Nyamagana Butera",
+                    "email": "nyamsprod@gmail.com",
+                    "homepage": "https://nyamsprod.com"
+                }
+            ],
+            "description": "userland URI parser RFC 3986 compliant",
+            "homepage": "https://github.com/thephpleague/uri-parser",
+            "keywords": [
+                "parse_url",
+                "parser",
+                "rfc3986",
+                "rfc3987",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/thephpleague/uri-parser/issues",
+                "source": "https://github.com/thephpleague/uri-parser/tree/master"
+            },
+            "time": "2018-11-22T07:55:51+00:00"
+        },
         {
             "name": "localheinz/diff",
             "version": "1.1.1",
@@ -3355,6 +4264,52 @@
             },
             "time": "2019-03-08T08:55:37+00:00"
         },
+        {
+            "name": "redisent/redisent",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jdp/redisent.git",
+                "reference": "0a9e40710153a5f36fbafe7d1f7fe0de3f723be4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jdp/redisent/zipball/0a9e40710153a5f36fbafe7d1f7fe0de3f723be4",
+                "reference": "0a9e40710153a5f36fbafe7d1f7fe0de3f723be4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-sockets": "*",
+                "php": ">=5.3.0"
+            },
+            "default-branch": true,
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "redisent": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Justin Poliey",
+                    "homepage": "http://justinpoliey.com/"
+                }
+            ],
+            "description": "Redisent",
+            "homepage": "https://github.com/jdp/redisent",
+            "keywords": [
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/jdp/redisent/issues",
+                "source": "https://github.com/jdp/redisent/tree/master"
+            },
+            "time": "2013-11-19T04:55:46+00:00"
+        },
         {
             "name": "roave/security-advisories",
             "version": "dev-latest",
@@ -4536,6 +5491,7 @@
                     "type": "github"
                 }
             ],
+            "abandoned": true,
             "time": "2020-11-30T07:30:19+00:00"
         },
         {
@@ -6506,6 +7462,7 @@
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
+        "redisent/redisent": 20,
         "roave/security-advisories": 20,
         "ukko/phpredis-phpdoc": 20
     },
diff --git a/src/Redis/Client/AbstractClient.php b/src/Redis/Client/AbstractClient.php
new file mode 100644
index 0000000..e7a6a2c
--- /dev/null
+++ b/src/Redis/Client/AbstractClient.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use function count;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+
+abstract class AbstractClient implements Client
+{
+    public function pipeline(Command ...$commands): array
+    {
+        $results = $this->doPipeline(...$commands);
+
+        return array_map(static function ($result, $index) use ($commands) {
+            return $commands[$index]->parseResponse($result);
+        }, $results, array_keys($results));
+    }
+
+    /**
+     * @param Command ...$commands
+     *
+     * @return array<mixed>
+     */
+    abstract protected function doPipeline(Command ...$commands): array;
+
+    protected static function fcqnExists(string $fcqn): bool
+    {
+        return class_exists($fcqn) || interface_exists($fcqn);
+    }
+
+    /**
+     * @param array<string,array<string>> $classesMethods
+     * @param array<string>               $functions
+     */
+    protected function getMissingMessage(string $name, bool $isExtension, array $classesMethods, array $functions = []): string
+    {
+        $classes = array_keys($classesMethods);
+        $methods = array_map(static function (string $class, array $methods) {
+            return array_map(static function ($method) use ($class) {
+                return $class.'::'.$method;
+            }, $methods);
+        }, $classes, $classesMethods);
+        $methods = array_reduce($methods, static function ($flat, $methods) {
+            return array_merge($flat, $methods);
+        }, []);
+
+        $message = 'The dependency '.$name.' is missing.'.PHP_EOL.'Install the dependency';
+        if (true === $isExtension) {
+            $message = 'The extension '.$name.' is missing.'.PHP_EOL.'Install the extension';
+        }
+        $message .= ' or use a polyfill that provide';
+
+        $classesMessage = null;
+        if (1 === count($classes)) {
+            $classesMessage = ' the class "'.reset($classes).'"';
+        } elseif (count($classes) > 1) {
+            $classesMessage = ' the classes "'.implode('", "', $classes).'"';
+        }
+        $methodsMessage = null;
+        if (1 === count($methods)) {
+            $methodsMessage = ' the method "'.reset($methods).'"';
+        } elseif (count($methods) > 1) {
+            $methodsMessage = ' the methods "'.implode('", "', $methods).'"';
+        }
+        $functionsMessage = null;
+        if (1 === count($functions)) {
+            $functionsMessage = ' the function "'.reset($functions).'"';
+        } elseif (count($functions) > 1) {
+            $functionsMessage = ' the functions "'.implode('", "', $functions).'"';
+        }
+
+        $message .= implode(' and ', array_filter([$classesMessage, $methodsMessage, $functionsMessage]));
+
+        return $message;
+    }
+}
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
new file mode 100644
index 0000000..0989bb3
--- /dev/null
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use function Amp\Promise\all;
+use function Amp\Promise\wait;
+use Amp\Redis\Redis;
+use function function_exists;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class AmpRedisClient extends AbstractClient
+{
+    /** @var Redis */
+    private $redis;
+
+    private function __construct(Redis $redis)
+    {
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('Amp\Redis', false, [
+                Redis::class => ['query'],
+            ], ['\\Amp\\Promise\\wait', '\\Amp\\Promise\\all']));
+        }
+        $this->redis = $redis;
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    /*[]
+     * @inheritDoc
+     */
+    public function execute(Command $command)
+    {
+        $result = wait($this->redis->query($command->getId(), ...array_map('strval', $command->getArguments())));
+
+        return $command->parseResponse($result);
+    }
+
+    public function executeRaw(...$args)
+    {
+        return wait($this->redis->query(...array_map('strval', $args)));
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof Redis
+            && method_exists($redis, 'query')
+            && function_exists('\\Amp\\Promise\\wait')
+            && function_exists('\\Amp\\Promise\\all');
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        trigger_error('Warning, \\Amp\\Redis\\Redis don\'t use a real Redis Pipeline', E_USER_NOTICE);
+
+        return wait(all(
+            array_map(function (Command $command) {
+                return $this->redis->query($command->getId(), ...array_map('strval', $command->getArguments()));
+            }, $commands)
+        ));
+    }
+}
diff --git a/src/Redis/Client/CheprasovRedisClient.php b/src/Redis/Client/CheprasovRedisClient.php
new file mode 100644
index 0000000..3ddc522
--- /dev/null
+++ b/src/Redis/Client/CheprasovRedisClient.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use Closure;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use RedisClient\Client\AbstractRedisClient;
+use RedisClient\Pipeline\PipelineInterface;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class CheprasovRedisClient extends AbstractClient
+{
+    /** @var AbstractRedisClient */
+    private $redis;
+
+    private function __construct(AbstractRedisClient $redis)
+    {
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('Cheprasov Redis', false, [
+                AbstractRedisClient::class => ['executeRaw', 'pipeline'],
+                PipelineInterface::class => [],
+            ]));
+        }
+        $this->redis = $redis;
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    public function execute(Command $command)
+    {
+        $response = $this->redis->executeRaw(array_merge([$command->getId()], $command->getArguments()));
+
+        return $command->parseResponse($response);
+    }
+
+    public function executeRaw(...$args)
+    {
+        return $this->redis->executeRaw(array_map('strval', $args));
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof AbstractRedisClient
+            && static::fcqnExists(PipelineInterface::class)
+            && method_exists(AbstractRedisClient::class, 'executeRaw')
+            && method_exists(AbstractRedisClient::class, 'pipeline');
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        trigger_error('Warning, a workaround is used to enable custom command in pipeline for \\RedisClient\\Pipeline\\PipelineInterface', E_USER_NOTICE);
+
+        return $this->redis->pipeline(function (PipelineInterface $pipeline) use ($commands): void {
+            foreach ($commands as $command) {
+                $closure = Closure::fromCallable(function () use ($command): void {
+                    // @phpstan-ignore-next-line
+                    $this->returnCommand(array_merge([$command->getId()], $command->getArguments()), null);
+                });
+                $closure->call($pipeline);
+            }
+        });
+    }
+}
diff --git a/src/Redis/Client/ClientFacade.php b/src/Redis/Client/ClientFacade.php
index 3b1a276..b0328bd 100644
--- a/src/Redis/Client/ClientFacade.php
+++ b/src/Redis/Client/ClientFacade.php
@@ -21,9 +21,14 @@
 
 namespace MacFJA\RediSearch\Redis\Client;
 
+use Amp\Redis\Redis as AmpRedis;
+use Credis_Client as CredisRedis;
 use MacFJA\RediSearch\Redis\Client;
-use Predis\ClientInterface;
-use Redis;
+use Predis\ClientInterface as PredisRedis;
+use Redis as PhpredisRedis;
+use RedisClient\Client\AbstractRedisClient as CheprasovRedis;
+use redisent\Redis as RedisentRedis;
+use Rediska as RediskaRedis;
 use RuntimeException;
 
 /**
@@ -32,10 +37,19 @@
 class ClientFacade
 {
     /** @var array<string> */
-    private $factories = [PhpiredisClient::class, PhpredisClient::class, PredisClient::class];
+    private $factories = [
+        PredisClient::class,
+        CredisClient::class,
+        PhpredisClient::class,
+        PhpiredisClient::class,
+        CheprasovRedisClient::class,
+        RedisentClient::class,
+        RediskaClient::class,
+        AmpRedisClient::class,
+    ];
 
     /**
-     * @param ClientInterface|mixed|Redis|resource $redis
+     * @param AmpRedis|CheprasovRedis|CredisRedis|mixed|PhpredisRedis|PredisRedis|RedisentRedis|RediskaRedis|resource $redis
      */
     public function getClient($redis): Client
     {
diff --git a/src/Redis/Client/CredisClient.php b/src/Redis/Client/CredisClient.php
new file mode 100644
index 0000000..4150224
--- /dev/null
+++ b/src/Redis/Client/CredisClient.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use Credis_Client;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class CredisClient extends AbstractClient
+{
+    /** @var Credis_Client */
+    private $redis;
+
+    private function __construct(Credis_Client $redis)
+    {
+        if (!self::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('Credis', false, [
+                Credis_Client::class => ['__call', 'pipeline', 'exec'],
+            ]));
+        }
+        $this->redis = $redis;
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    public function execute(Command $command)
+    {
+        $result = $this->redis->__call($command->getId(), $command->getArguments());
+
+        return $command->parseResponse($result);
+    }
+
+    public function executeRaw(...$args)
+    {
+        $command = array_shift($args);
+
+        return $this->redis->__call($command, $args);
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof Credis_Client
+            && method_exists($redis, '__call')
+            && method_exists($redis, 'pipeline')
+            && method_exists($redis, 'exec');
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        $pipeline = $this->redis->pipeline();
+        foreach ($commands as $command) {
+            $pipeline = $pipeline->__call($command->getId(), $command->getArguments());
+        }
+
+        return $pipeline->exec();
+    }
+}
diff --git a/src/Redis/Client/PhpiredisClient.php b/src/Redis/Client/PhpiredisClient.php
index ef98cd2..8fe34a9 100644
--- a/src/Redis/Client/PhpiredisClient.php
+++ b/src/Redis/Client/PhpiredisClient.php
@@ -31,23 +31,26 @@
 /**
  * @codeCoverageIgnore
  */
-class PhpiredisClient implements Client
+class PhpiredisClient extends AbstractClient
 {
     /** @var resource */
     private $redis;
 
     /**
      * @param resource $redis
+     * @psalm-assert function_exists('phpiredis_command_bs')
+     * @psalm-assert function_exists('phpiredis_multi_command_bs')
      */
     private function __construct($redis)
     {
-        $this->validateEnvironment();
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('phpiredis', true, [], ['phpiredis_command_bs', 'phpiredis_multi_command_bs']));
+        }
         $this->redis = $redis;
     }
 
     public function execute(Command $command)
     {
-        $this->validateEnvironment();
         $rawResponse = phpiredis_command_bs($this->redis, array_merge([$command->getId()], $command->getArguments()));
 
         return $command->parseResponse($rawResponse);
@@ -78,34 +81,15 @@ public static function make($redis): Client
 
     public function executeRaw(...$args)
     {
-        $this->validateEnvironment();
-
         $args = array_map('strval', $args);
 
         return phpiredis_command_bs($this->redis, $args);
     }
 
-    public function pipeline(Command ...$commands): array
+    protected function doPipeline(Command ...$commands): array
     {
-        $results = phpiredis_multi_command_bs($this->redis, array_map(static function (Command $command) {
+        return phpiredis_multi_command_bs($this->redis, array_map(static function (Command $command) {
             return array_merge([$command->getId()], $command->getArguments());
         }, $commands));
-
-        return array_map(static function ($result, $index) use ($commands) {
-            return $commands[$index]->parseResponse($result);
-        }, $results, array_keys($results));
-    }
-
-    /**
-     * @psalm-assert function_exists('phpiredis_command')
-     */
-    private function validateEnvironment(): void
-    {
-        if (!function_exists('phpiredis_command_bs') || !function_exists('phpiredis_multi_command_bs')) {
-            throw new RuntimeException(
-                'The extension phpiredis is missing.'.PHP_EOL.
-                'Install the extension or use a polyfill that provide the functions "phpiredis_command_bs" and "phpiredis_multi_command_bs"'
-            );
-        }
     }
 }
diff --git a/src/Redis/Client/PhpredisClient.php b/src/Redis/Client/PhpredisClient.php
index 0f0d406..fffb081 100644
--- a/src/Redis/Client/PhpredisClient.php
+++ b/src/Redis/Client/PhpredisClient.php
@@ -30,23 +30,17 @@
 /**
  * @codeCoverageIgnore
  */
-class PhpredisClient implements Client
+class PhpredisClient extends AbstractClient
 {
     /** @var Redis */
     private $redis;
 
     private function __construct(Redis $redis)
     {
-        if (
-            !method_exists(Redis::class, 'rawCommand')
-            || !method_exists(Redis::class, 'multi')
-            || !method_exists(Redis::class, 'exec')
-        ) {
-            throw new RuntimeException(
-                'The extension phpredis is missing.'.PHP_EOL.
-                'Install the extension or use a polyfill that provide the class "\\Redis" and the functions '.
-                '"\\Redis::rawCommand" and "\\Redis::multi" and "\\Redis::exec"'
-            );
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('phpredis', true, [
+                Redis::class => ['rawCommand', 'multi', 'exec'],
+            ]));
         }
         $this->redis = $redis;
     }
@@ -87,7 +81,7 @@ public function executeRaw(...$args)
         return $this->redis->rawCommand(...$args);
     }
 
-    public function pipeline(Command ...$commands): array
+    protected function doPipeline(Command ...$commands): array
     {
         $pipeline = $this->redis->multi();
         foreach ($commands as $command) {
@@ -97,10 +91,7 @@ public function pipeline(Command ...$commands): array
             }
             $pipeline = $pipeline->rawCommand($command->getId(), ...$arguments);
         }
-        $results = $pipeline->exec();
 
-        return array_map(static function ($result, $index) use ($commands) {
-            return $commands[$index]->parseResponse($result);
-        }, $results, array_keys($results));
+        return $pipeline->exec();
     }
 }
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
index 9412b86..b1c66d1 100644
--- a/src/Redis/Client/PredisClient.php
+++ b/src/Redis/Client/PredisClient.php
@@ -32,27 +32,19 @@
 /**
  * @codeCoverageIgnore
  */
-class PredisClient implements Client
+class PredisClient extends AbstractClient
 {
     /** @var ClientInterface */
     private $redis;
 
     private function __construct(ClientInterface $redis)
     {
-        if (
-            !static::fcqnExists(\Predis\Command\Command::class)
-            || !static::fcqnExists(ClientContextInterface::class)
-            || !method_exists(ClientInterface::class, 'executeCommand')
-            || !method_exists($redis, 'pipeline')
-            || !method_exists(ClientContextInterface::class, 'executeCommand')
-        ) {
-            throw new RuntimeException(
-                'Dependency Predis is missing.'.PHP_EOL.
-                'Install the dependency or use a polyfill that provide the classes'.
-                ' "\\Predis\\Command\\Command" and "\\Predis\\ClientInterface" and "\\Predis\\ClientContextInterface"'.
-                ' and functions "\\Predis\\ClientInterface::executeCommand" and "\\Predis\\ClientInterface::pipeline"'.
-                ' and "\\Predis\\ClientContextInterface::executeCommand"'
-            );
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('Predis', false, [
+                RawCommand::class => [],
+                ClientInterface::class => ['executeCommand', 'pipeline'],
+                ClientContextInterface::class => ['executeCommand'],
+            ]));
         }
         $this->redis = $redis;
     }
@@ -69,13 +61,15 @@ public static function supports($redis): bool
         if (
             !static::fcqnExists(ClientInterface::class)
             || !static::fcqnExists(RawCommand::class)
+            || !static::fcqnExists(ClientContextInterface::class)
         ) {
             return false;
         }
 
         return $redis instanceof ClientInterface
             && method_exists($redis, 'executeCommand')
-            && method_exists($redis, 'pipeline');
+            && method_exists($redis, 'pipeline')
+            && method_exists(ClientContextInterface::class, 'executeCommand');
     }
 
     public static function make($redis): Client
@@ -91,22 +85,14 @@ public function executeRaw(...$args)
         return $this->redis->executeCommand(new RawCommand($args));
     }
 
-    public function pipeline(Command ...$commands): array
+    protected function doPipeline(Command ...$commands): array
     {
         assert(method_exists($this->redis, 'pipeline'));
-        $results = $this->redis->pipeline(static function (ClientContextInterface $pipeline) use ($commands): void {
+
+        return $this->redis->pipeline(static function (ClientContextInterface $pipeline) use ($commands): void {
             foreach ($commands as $command) {
                 $pipeline->executeCommand(new RawCommand(array_merge([$command->getId()], $command->getArguments())));
             }
         });
-
-        return array_map(static function ($result, $index) use ($commands) {
-            return $commands[$index]->parseResponse($result);
-        }, $results, array_keys($results));
-    }
-
-    private static function fcqnExists(string $fcqn): bool
-    {
-        return class_exists($fcqn) || interface_exists($fcqn);
     }
 }
diff --git a/src/Redis/Client/RedisentClient.php b/src/Redis/Client/RedisentClient.php
new file mode 100644
index 0000000..1d1b46e
--- /dev/null
+++ b/src/Redis/Client/RedisentClient.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use redisent\Redis;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class RedisentClient extends AbstractClient
+{
+    /** @var Redis */
+    private $redis;
+
+    private function __construct(Redis $redis)
+    {
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('Redisent', false, [
+                Redis::class => ['__call', 'pipeline', 'uncork'],
+            ]));
+        }
+        $this->redis = $redis;
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    public function execute(Command $command)
+    {
+        $result = $this->redis->__call($command->getId(), $command->getArguments());
+
+        return $command->parseResponse($result);
+    }
+
+    public function executeRaw(...$args)
+    {
+        $command = array_shift($args);
+
+        return $this->redis->__call($command, $args);
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof Redis
+            && method_exists($redis, '__call')
+            && method_exists($redis, 'pipeline')
+            && method_exists($redis, 'uncork');
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        $pipeline = $this->redis->pipeline();
+        foreach ($commands as $command) {
+            $pipeline = $pipeline->__call($command->getId(), $command->getArguments());
+        }
+
+        return $pipeline->uncork();
+    }
+}
diff --git a/src/Redis/Client/RediskaClient.php b/src/Redis/Client/RediskaClient.php
new file mode 100644
index 0000000..ea867ab
--- /dev/null
+++ b/src/Redis/Client/RediskaClient.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use function in_array;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use MacFJA\RediSearch\Redis\Initializer;
+use Rediska;
+use Rediska_Command_Abstract;
+use Rediska_Commands;
+use Rediska_Connection_Exec;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class RediskaClient extends AbstractClient
+{
+    /** @var Rediska */
+    private $redis;
+
+    private function __construct(Rediska $redis)
+    {
+        if (!static::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage('Rediska', false, [
+                Rediska::class => ['__class', 'pipeline'],
+                Rediska_Command_Abstract::class => [],
+                Rediska_Connection_Exec::class => [],
+                Rediska_Commands::class => ['add', 'getList'],
+            ]));
+        }
+        Initializer::registerCommandsRediska();
+        $this->redis = $redis;
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    public function execute(Command $command)
+    {
+        $result = $this->redis->__call('__redisearch', array_merge([$command->getId()], $command->getArguments()));
+
+        return $command->parseResponse($result);
+    }
+
+    public function executeRaw(...$args)
+    {
+        $command = (string) $args[0];
+        $commands = array_keys(Rediska_Commands::getList());
+
+        if (in_array(strtolower($command), $commands, true)) {
+            array_shift($args);
+
+            return $this->redis->__call($command, $args);
+        }
+
+        return $this->redis->__call('__redisearch', $args);
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof Rediska
+            && static::fcqnExists(Rediska_Commands::class)
+            && static::fcqnExists(Rediska_Command_Abstract::class)
+            && static::fcqnExists(Rediska_Connection_Exec::class)
+            && method_exists($redis, '__call')
+            && method_exists(Rediska_Commands::class, 'add')
+            && method_exists($redis, 'pipeline');
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        $pipeline = $this->redis->pipeline();
+        foreach ($commands as $command) {
+            $pipeline = $pipeline->__call('__redisearch', array_merge([$command->getId()], $command->getArguments()));
+        }
+
+        return $pipeline->execute();
+    }
+}
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 4993aa0..d96337c 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\Redis;
 
+use function count;
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
 use MacFJA\RediSearch\Redis\Command\AliasDel;
@@ -49,6 +50,34 @@
 use MacFJA\RediSearch\Redis\Command\SynUpdate;
 use MacFJA\RediSearch\Redis\Command\TagVals;
 use Predis\Profile\RedisProfile;
+use Rediska_Command_Abstract;
+use Rediska_Commands;
+use Rediska_Connection_Exec;
+
+if (class_exists(Rediska_Command_Abstract::class) && class_exists(Rediska_Connection_Exec::class)) {
+    /**
+     * @codeCoverageIgnore
+     */
+    class RediskaRediSearch extends Rediska_Command_Abstract
+    {
+        public function create(): Rediska_Connection_Exec
+        {
+            $connections = $this->_rediska->getConnections();
+
+            if (count($connections) > 1) {
+                trigger_error('Warning, Multiple redis connections exists, only the first connection will be used', E_USER_NOTICE);
+            }
+            $connection = reset($connections);
+
+            $commands = $this->_arguments;
+            if (!('__redisearch' === $this->_name)) {
+                array_unshift($commands, $this->_name);
+            }
+
+            return new Rediska_Connection_Exec($connection, $commands);
+        }
+    }
+}
 
 /**
  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -58,7 +87,7 @@ class Initializer
     /**
      * @codeCoverageIgnore
      */
-    public static function registerCommands(RedisProfile $profile): void
+    public static function registerCommandsPredis(RedisProfile $profile): void
     {
         $profile->defineCommand('ftaggregate', Aggregate::class);
         $profile->defineCommand('ftaliasadd', AliasAdd::class);
@@ -89,15 +118,50 @@ public static function registerCommands(RedisProfile $profile): void
         $profile->defineCommand('fttagvals', TagVals::class);
     }
 
+    /**
+     * @codeCoverageIgnore
+     */
+    public static function registerCommandsRediska(): void
+    {
+        Rediska_Commands::add('ftaggregate', RediskaRediSearch::class);
+        Rediska_Commands::add('ftaliasadd', RediskaRediSearch::class);
+        Rediska_Commands::add('ftaliasdel', RediskaRediSearch::class);
+        Rediska_Commands::add('ftaliasupdate', RediskaRediSearch::class);
+        Rediska_Commands::add('ftalter', RediskaRediSearch::class);
+        Rediska_Commands::add('ftconfig', RediskaRediSearch::class);
+        Rediska_Commands::add('ftconfigset', RediskaRediSearch::class);
+        Rediska_Commands::add('ftcreate', RediskaRediSearch::class);
+        Rediska_Commands::add('ftcursordel', RediskaRediSearch::class);
+        Rediska_Commands::add('ftcursorread', RediskaRediSearch::class);
+        Rediska_Commands::add('ftdictadd', RediskaRediSearch::class);
+        Rediska_Commands::add('ftdictdel', RediskaRediSearch::class);
+        Rediska_Commands::add('ftdictdump', RediskaRediSearch::class);
+        Rediska_Commands::add('ftdropindex', RediskaRediSearch::class);
+        Rediska_Commands::add('ftexplain', RediskaRediSearch::class);
+        Rediska_Commands::add('ftexplaincli', RediskaRediSearch::class);
+        Rediska_Commands::add('ftlist', RediskaRediSearch::class);
+        Rediska_Commands::add('ftinfo', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsearch', RediskaRediSearch::class);
+        Rediska_Commands::add('ftspellcheck', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsugadd', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsugdel', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsugget', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsuglen', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsyndump', RediskaRediSearch::class);
+        Rediska_Commands::add('ftsynupdate', RediskaRediSearch::class);
+        Rediska_Commands::add('fttagvals', RediskaRediSearch::class);
+        Rediska_Commands::add('__redisearch', RediskaRediSearch::class);
+    }
+
     public static function getRediSearchVersion(Client $client): ?string
     {
-        $modules = $client->executeRaw('info', 'Modules')['Modules'] ?? [];
+        $modules = $client->executeRaw('module', 'list') ?? [];
 
         foreach ($modules as $module) {
             $data = array_column(
-                array_map(
-                    static function ($line) { return explode('=', $line); },
-                    explode(',', $module)
+                array_chunk(
+                    $module,
+                    2
                 ),
                 1,
                 0
diff --git a/src/Redis/Response/InfoResponse.php b/src/Redis/Response/InfoResponse.php
index 3d7a7ac..410d835 100644
--- a/src/Redis/Response/InfoResponse.php
+++ b/src/Redis/Response/InfoResponse.php
@@ -79,7 +79,7 @@ class InfoResponse implements Response
     /**
      * @param array<array<mixed>|float|int|string> $rawLines
      */
-    public function __construct($rawLines)
+    public function __construct(array $rawLines)
     {
         $this->rawLines = $rawLines;
     }
@@ -233,7 +233,7 @@ private static function setOptionValue(CreateCommandFieldOption $commandOption,
      */
     private static function setOptionFlag(CreateCommandFieldOption $commandOption, array $fields, string $key, string $optionName): CreateCommandFieldOption
     {
-        assert($commandOption instanceof TextFieldOption || $commandOption instanceof TagFieldOption);
+        assert($commandOption instanceof GroupedOption);
         if (in_array($key, $fields, true)) {
             $commandOption->setDataOfOption($optionName, true);
         }
diff --git a/tests/Redis/InitializerTest.php b/tests/Redis/InitializerTest.php
index 03e2c66..7ebfdac 100644
--- a/tests/Redis/InitializerTest.php
+++ b/tests/Redis/InitializerTest.php
@@ -39,7 +39,7 @@ class InitializerTest extends \PHPUnit\Framework\TestCase
     public function testRediSearchVersion(array $input, ?string $expected): void
     {
         $client = $this->createMock(Client::class);
-        $client->method('executeRaw')->with('info', 'Modules')->willReturn(['Modules' => $input]);
+        $client->method('executeRaw')->with('module', 'list')->willReturn($input);
 
         static::assertSame($expected, Initializer::getRediSearchVersion($client));
     }
@@ -50,13 +50,13 @@ public function testRediSearchVersion(array $input, ?string $expected): void
     public function dataProvider(string $testName): Generator
     {
         if ('testRediSearchVersion' === $testName) {
-            yield [['name=search,ver=20005,api=1,filters=0,usedby=[],using=[],options=[]'], '2.0.5'];
-            yield [['name=search,ver=20000,api=1,filters=0,usedby=[],using=[],options=[]'], '2.0.0'];
-            yield [['name=search,ver=26512,api=1,filters=0,usedby=[],using=[],options=[]'], '2.65.12'];
-            yield [['name=search,ver=120569,api=1,filters=0,usedby=[],using=[],options=[]'], '12.5.69'];
-            yield [['name=search,ver=99999,api=1,filters=0,usedby=[],using=[],options=[]'], '9.99.99'];
-            yield [['name=json,ver=99999,api=1,filters=0,usedby=[],using=[],options=[]'], null];
-            yield [['name=json,ver=99999,api=1,filters=0,usedby=[],using=[],options=[]', 'name=search,ver=20005,api=1,filters=0,usedby=[],using=[],options=[]'], '2.0.5'];
+            yield [[['name', 'search', 'ver', '20005']], '2.0.5'];
+            yield [[['name', 'search', 'ver', '20000']], '2.0.0'];
+            yield [[['name', 'search', 'ver', '26512']], '2.65.12'];
+            yield [[['name', 'search', 'ver', '120569']], '12.5.69'];
+            yield [[['name', 'search', 'ver', '99999']], '9.99.99'];
+            yield [[['name', 'json', 'ver', '99999']], null];
+            yield [[['name', 'json', 'ver', '99999'], ['name', 'search', 'ver', '20005']], '2.0.5'];
         }
     }
 }

From 764fd385260534a8c98a241e7371b3b0223b5c77 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 17 Oct 2021 03:27:46 +0200
Subject: [PATCH 15/60] Add integration tests

---
 Makefile                                 |   7 +-
 phpunit.xml.dist                         |  10 +
 src/Redis/Command/Aggregate.php          |   4 +-
 src/Redis/Command/Search.php             |   4 +-
 src/Redis/Response/PaginatedResponse.php |   8 +-
 tests/integration/DockerTest.php         | 228 +++++++++++++++++++++++
 6 files changed, 254 insertions(+), 7 deletions(-)
 create mode 100644 tests/integration/DockerTest.php

diff --git a/Makefile b/Makefile
index 426198f..7d54984 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: analyze fix-code test coverage mutation-test validation
+.PHONY: analyze fix-code test coverage validation integration-test
 
 analyze: | vendor
 	$(COMPOSER) exec -v parallel-lint -- src
@@ -22,7 +22,10 @@ coverage: | vendor
 	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
 	$(COMPOSER) exec -v phpunit -- --coverage-text --color
 
-validation: fix-code analyze test coverage
+integration-test: | vendor
+	$(COMPOSER) exec -v phpunit -- --group integration
+
+validation: fix-code analyze test coverage integration-test
 
 vendor: composer.json
 	$(COMPOSER) install --optimize-autoloader --no-suggest --prefer-dist
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e1e87b0..d20a507 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,9 +11,19 @@
     <testsuites>
         <testsuite name="default">
             <directory suffix="Test.php">tests</directory>
+            <exclude>tests/integration</exclude>
+        </testsuite>
+        <testsuite name="integration">
+            <directory suffix="Test.php">tests/integration</directory>
         </testsuite>
     </testsuites>
 
+    <groups>
+        <exclude>
+            <group>integration</group>
+        </exclude>
+    </groups>
+
     <filter>
         <whitelist addUncoveredFilesFromWhitelist="true" processUncoveredFilesFromWhitelist="true">
             <directory suffix=".php">src</directory>
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 5656581..c3b530f 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -22,7 +22,6 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function assert;
-use function is_int;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\LimitOption;
@@ -198,7 +197,8 @@ protected function transformParsedResponse($data)
         }
 
         $totalCount = array_shift($data);
-        assert(is_int($totalCount));
+        assert(is_numeric($totalCount));
+        $totalCount = (int) $totalCount;
 
         $items = array_map(static function (array $document) {
             return new AggregateResponseItem(self::getPairs($document));
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 5dba9b3..3dddb3c 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -25,7 +25,6 @@
 use function count;
 use InvalidArgumentException;
 use function is_array;
-use function is_int;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
@@ -315,7 +314,8 @@ public function transformParsedResponse($data)
         }
 
         $totalCount = array_shift($data);
-        assert(is_int($totalCount));
+        assert(is_numeric($totalCount));
+        $totalCount = (int) $totalCount;
 
         $useScores = $this->options['withscores']->isActive();
         $usePayloads = $this->options['withpayloads']->isActive();
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index 222e576..f800850 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Response;
 
 use function count;
+use Countable;
 use function is_int;
 use Iterator;
 use MacFJA\RediSearch\Redis\Client;
@@ -31,7 +32,7 @@
 /**
  * @implements Iterator<int,AggregateResponseItem[]|SearchResponseItem[]>
  */
-class PaginatedResponse implements Response, Iterator
+class PaginatedResponse implements Response, Iterator, Countable
 {
     /** @var array<AggregateResponseItem>|array<SearchResponseItem> */
     private $items;
@@ -125,6 +126,11 @@ public function getTotalCount(): int
         return $this->totalCount;
     }
 
+    public function count()
+    {
+        return $this->totalCount;
+    }
+
     private function updateWithLimit(int $offset, int $size): void
     {
         /** @var PaginatedCommand $nextCommand */
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
new file mode 100644
index 0000000..78c64c2
--- /dev/null
+++ b/tests/integration/DockerTest.php
@@ -0,0 +1,228 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\integration;
+
+use Amp\Redis\Config;
+use Amp\Redis\RemoteExecutor;
+use Closure;
+use MacFJA\RediSearch\Index;
+use MacFJA\RediSearch\IndexBuilder;
+use MacFJA\RediSearch\Query\Builder;
+use MacFJA\RediSearch\Query\Builder\Fuzzy;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
+use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
+use MacFJA\RediSearch\Redis\Command\DropIndex;
+use MacFJA\RediSearch\Redis\Command\IndexList;
+use MacFJA\RediSearch\Redis\Command\Search;
+use MacFJA\RediSearch\Redis\Command\SynDump;
+use MacFJA\RediSearch\Redis\Command\SynUpdate;
+use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestResult;
+use Rediska;
+
+/**
+ * @covers \MacFJA\RediSearch\Index
+ * @covers \MacFJA\RediSearch\IndexBuilder
+ * @covers \MacFJA\RediSearch\Query\Builder
+ * @covers \MacFJA\RediSearch\Redis\Client\AbstractClient
+ * @covers \MacFJA\RediSearch\Redis\Client\AmpRedisClient
+ * @covers \MacFJA\RediSearch\Redis\Client\CheprasovRedisClient
+ * @covers \MacFJA\RediSearch\Redis\Client\PredisClient
+ * @covers \MacFJA\RediSearch\Redis\Client\RedisentClient
+ * @covers \MacFJA\RediSearch\Redis\Client\RediskaClient
+ * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
+ * @covers \MacFJA\RediSearch\Redis\Command\DropIndex
+ * @covers \MacFJA\RediSearch\Redis\Command\IndexList
+ * @covers \MacFJA\RediSearch\Redis\Command\Info
+ * @covers \MacFJA\RediSearch\Redis\Command\Search
+ * @covers \MacFJA\RediSearch\Redis\Command\SynDump
+ * @covers \MacFJA\RediSearch\Redis\Command\SynUpdate
+ * @covers \MacFJA\RediSearch\Redis\Initializer
+ * @covers \MacFJA\RediSearch\Redis\RediskaRediSearch
+ * @covers \MacFJA\RediSearch\Redis\Response\InfoResponse
+ * @covers \MacFJA\RediSearch\Redis\Response\PaginatedResponse
+ *
+ * @uses \MacFJA\RediSearch\Query\Builder\AbstractGroup
+ * @uses \MacFJA\RediSearch\Query\Builder\AndGroup
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor
+ * @uses \MacFJA\RediSearch\Redis\Command\Create
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\BaseCreateFieldOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\PrefixFieldName
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Query\Builder\Fuzzy
+ * @uses \MacFJA\RediSearch\Query\Escaper
+ *
+ * @group integration
+ *
+ * @internal
+ */
+class DockerTest extends TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        parent::setUpBeforeClass();
+        exec('which docker', $output, $code);
+        if ($code > 0) {
+            static::markTestSkipped('Docker is missing');
+        }
+        exec('docker run --rm -p 16379:6379 -d redislabs/redisearch:latest');
+    }
+
+    public static function tearDownAfterClass(): void
+    {
+        parent::tearDownAfterClass();
+        exec('docker ps --filter publish=16379 -q', $output);
+        foreach ($output as $container) {
+            exec('docker stop '.escapeshellarg($container), $output);
+        }
+    }
+
+    /**
+     * @dataProvider dataProvider
+     * @large
+     *
+     * @param mixed $clientBuilder
+     */
+    public function testIntegration($clientBuilder): void
+    {
+        $client = $clientBuilder();
+        $resultObject = $this->getTestResultObject();
+        if ($resultObject instanceof TestResult) {
+            $resultObject->beStrictAboutOutputDuringTests(false);
+            $resultObject->convertNoticesToExceptions(false);
+        }
+
+        $list = $client->execute(new IndexList());
+        static::assertEmpty($list);
+        $builder = new IndexBuilder();
+        $builder
+            ->withIndex('testDoc')
+            ->addTextField('lastname', false, null, null, true)
+            ->addTextField('firstname')
+            ->addNumericField('age')
+            ->create($client)
+        ;
+        static::assertEquals(['testDoc'], $client->execute(new IndexList()));
+
+        $index = new Index('testDoc', $client);
+        $docToRemove = [];
+        $docToRemove[] = $index->addDocumentFromArray(['firstname' => 'Joe', 'lastname' => 'Doe', 'age' => 30]);
+        $docToRemove[] = $index->addDocumentFromArray(['firstname' => 'Joe', 'age' => 30]);
+        $docToRemove[] = $index->addDocumentFromArray(['firstname' => 'Joe', 'age' => 30, 'fullname' => 'eeeee']);
+
+        $search = new Search();
+
+        $query = (new Builder())
+            ->addElement(new Fuzzy('Joo'))
+        ;
+
+        $search
+            ->setIndex('testDoc')
+            ->setQuery($query->render())
+            ->setWithPayloads()
+            ->setHighlight(['lastname'], '<b>', '</b>')
+            ->setWithScores()
+        ;
+
+        /** @var PaginatedResponse $result */
+        $result = $client->execute($search);
+        static::assertCount(3, $result);
+        $client->execute((new SynUpdate())->setIndex('testDoc')->setGroupId('Joe')->setTerms('John'));
+        $syns = $client->execute((new SynDump())->setIndex('testDoc'));
+        static::assertEquals([
+            'john', ['Joe'],
+        ], $syns);
+
+        $result = $client->pipeline(
+            (new Search())
+                ->setIndex('testDoc')
+                ->setQuery('%%Joe%%')
+                ->setWithPayloads()
+                ->setWithScores()
+                ->setHighlight(['lastname'], '<b>', '</b>'),
+            (new Aggregate())
+                ->setIndex('testDoc')
+                ->setQuery('*')
+                ->addGroupBy(new GroupByOption(['lastname'], [ReduceOption::count('count')])),
+            (new Aggregate())
+                ->setIndex('testDoc')
+                ->setQuery('*')
+                ->addGroupBy(new GroupByOption(['firstname'], [ReduceOption::count('count')])),
+            (new Aggregate())
+                ->setIndex('testDoc')
+                ->setQuery('*')
+                ->addGroupBy(new GroupByOption(['age'], [ReduceOption::count('count')])),
+            (new Aggregate())
+                ->setIndex('testDoc')
+                ->setQuery('*')
+                ->addGroupBy(new GroupByOption([], [ReduceOption::toList('age', 'list')]))
+        );
+
+        static::assertCount(3, $result[0]);
+        static::assertEquals('1', current($result[1])[0]->getValue('count'));
+        static::assertEquals('2', current($result[1])[1]->getValue('count'));
+        static::assertEquals('3', current($result[2])[0]->getValue('count'));
+        static::assertEquals('3', current($result[3])[0]->getValue('count'));
+        static::assertEquals(['30'], current($result[4])[0]->getValue('list'));
+
+        $client->execute((new DropIndex())->setIndex('testDoc')->setDeleteDocument());
+        $client->executeRaw('del', ...$docToRemove);
+    }
+
+    /**
+     * @return Closure[][]
+     */
+    public function dataProvider(): array
+    {
+        return [
+            [static function () { return Client\PredisClient::make(new \Predis\Client(['scheme' => 'tcp', 'host' => 'localhost', 'port' => '16379', 'db' => 0])); }],
+            [static function () { return Client\RediskaClient::make(new Rediska(['servers' => [['host' => 'localhost', 'port' => '16379', 'db' => 0]]])); }],
+            [static function () { return Client\RedisentClient::make(new \redisent\Redis('redis://localhost:16379')); }],
+            [static function () { return Client\CheprasovRedisClient::make(new \RedisClient\Client\Version\RedisClient6x0(['server' => 'localhost:16379', 'database' => 0])); }],
+            [static function () { return Client\AmpRedisClient::make(new \Amp\Redis\Redis(new RemoteExecutor(Config::fromUri('redis://localhost:16379')))); }],
+        ];
+    }
+}

From e1088959aabe2663ab09ff57eff449e3cfddfee4 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 17 Oct 2021 15:43:02 +0200
Subject: [PATCH 16/60] Add TinyRedis + update Readme

---
 README.md                                 | 26 ++++++-
 composer.json                             |  4 +-
 composer.lock                             | 45 +++++++++++-
 src/Redis/Client/AbstractClient.php       | 18 +++--
 src/Redis/Client/AmpRedisClient.php       |  6 +-
 src/Redis/Client/CheprasovRedisClient.php |  3 +-
 src/Redis/Client/CredisClient.php         | 32 +++++++--
 src/Redis/Client/TinyRedisClient.php      | 87 +++++++++++++++++++++++
 src/Redis/Initializer.php                 |  3 +-
 tests/integration/DockerTest.php          | 12 ++--
 10 files changed, 207 insertions(+), 29 deletions(-)
 create mode 100644 src/Redis/Client/TinyRedisClient.php

diff --git a/README.md b/README.md
index 3018080..84ae2e8 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,12 @@ This lib can use several connector for Redis:
  - [Predis](https://github.com/predis/predis/wiki) - Pure PHP implementation
  - [Phpredis](https://github.com/phpredis/phpredis) - PHP extension
  - [Phpiredis](https://github.com/nrk/phpiredis) - PHP extension depending on [hiredis](https://github.com/redis/hiredis)
+ - [Amp\Redis](https://github.com/amphp/redis) - Pure PHP Async implementation
+ - [cheprasov/php-redis-client](https://github.com/cheprasov/php-redis-client) - Pure PHP implementation
+ - [Credis](https://github.com/colinmollenhour/credis) - Pure PHP implementation
+ - [Rediska](https://github.com/Shumkov/Rediska) - Pure PHP implementation
+ - [Redisent](https://github.com/jdp/redisent) - Pure PHP implementation
+ - [TinyRedis](https://github.com/ptrofimov/tinyredisclient) - Pure PHP implementation
 
 You can pick the connector depending of your need.
 
@@ -28,10 +34,28 @@ $clientFacade = new \MacFJA\RediSearch\Redis\Client\ClientFacade();
 $client = $clientFacade->getClient(new \Predis\Client(/* ... */));
 
 // With Phpredis extension
-$client = $clientFacade->getClient(new Redis([/* ... */]));
+$client = $clientFacade->getClient(new \Redis([/* ... */]));
 
 // With Phpiredis extension
 $client = $clientFacade->getClient(phpiredis_connect($host));
+
+// With Amp\Redis
+$client = $clientFacade->getClient(new \Amp\Redis\Redis(new RemoteExecutor(Config::fromUri(/* ... */))));
+
+// With Cheprasov
+$client = $clientFacade->getClient(new \RedisClient\Client\Version\RedisClient6x0([/* ... */]));
+
+// With Rediska
+$client = $clientFacade->getClient(new \Rediska(['servers' => [[/* ... */]]]));
+
+// With Redisent
+$client = $clientFacade->getClient(new \redisent\Redis(/* ... */));
+
+// With TinyRedisClient
+$client = $clientFacade->getClient(new \TinyRedisClient(/* ... */));
+
+// With Credis
+$client = $clientFacade->getClient(new \Credis_Client(/* ... */));
 ```
 
 You can add your own implementation, all you need is to implement the interface `\MacFJA\RediSearch\Redis\Client` and add it to the client facace with:
diff --git a/composer.json b/composer.json
index 6cc14a3..0df4050 100644
--- a/composer.json
+++ b/composer.json
@@ -35,6 +35,7 @@
         "phpstan/phpstan": "^0.12.92",
         "phpunit/phpunit": "^8.5",
         "predis/predis": "^1.1",
+        "ptrofimov/tinyredisclient": "^1.1",
         "redisent/redisent": "dev-master",
         "roave/security-advisories": "dev-latest",
         "rskuipers/php-assumptions": "^0.8.0",
@@ -44,12 +45,13 @@
     },
     "suggest": {
         "ext-phpiredis": "To use Phpiredis extension implementation",
-        "ext-phpredis": "To use Phpredis extension implementation",
+        "ext-redis": "To use Phpredis extension implementation",
         "amphp/redis": "To use AmpPhp implementation",
         "cheprasov/php-redis-client": "To use Cheprasov implementation",
         "colinmollenhour/credis": "To use Credis implementation",
         "geometria-lab/rediska": "To use Rediska implementation",
         "predis/predis": "To use Predis implementation",
+        "ptrofimov/tinyredisclient": "To use TinyRedisClient implementation",
         "redisent/redisent": "To use Redisent implementation"
     },
     "config": {
diff --git a/composer.lock b/composer.lock
index acdc1c0..97bd7eb 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "6fe468a3d47a4506b963d1d03e7c38af",
+    "content-hash": "7bb89cff485836217af5df89eec3ad68",
     "packages": [
         {
             "name": "composer/semver",
@@ -4220,6 +4220,49 @@
             },
             "time": "2021-05-03T11:20:27+00:00"
         },
+        {
+            "name": "ptrofimov/tinyredisclient",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ptrofimov/tinyredisclient.git",
+                "reference": "7b8d487afa41b74e30d1a68400d89c12d9b82b6f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ptrofimov/tinyredisclient/zipball/7b8d487afa41b74e30d1a68400d89c12d9b82b6f",
+                "reference": "7b8d487afa41b74e30d1a68400d89c12d9b82b6f",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Petr Trofimov",
+                    "email": "petrofimov@yandex.ru",
+                    "homepage": "https://github.com/ptrofimov"
+                }
+            ],
+            "description": "The most lightweight Redis client written in PHP",
+            "homepage": "https://github.com/ptrofimov/tinyredisclient",
+            "keywords": [
+                "client",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/ptrofimov/tinyredisclient/issues",
+                "source": "https://github.com/ptrofimov/tinyredisclient/tree/master"
+            },
+            "time": "2013-04-09T19:16:58+00:00"
+        },
         {
             "name": "ralouphie/getallheaders",
             "version": "3.0.3",
diff --git a/src/Redis/Client/AbstractClient.php b/src/Redis/Client/AbstractClient.php
index e7a6a2c..470ac6b 100644
--- a/src/Redis/Client/AbstractClient.php
+++ b/src/Redis/Client/AbstractClient.php
@@ -27,6 +27,9 @@
 
 abstract class AbstractClient implements Client
 {
+    /** @var bool */
+    public static $disableNotice = false;
+
     public function pipeline(Command ...$commands): array
     {
         $results = $this->doPipeline(...$commands);
@@ -68,28 +71,29 @@ protected function getMissingMessage(string $name, bool $isExtension, array $cla
         if (true === $isExtension) {
             $message = 'The extension '.$name.' is missing.'.PHP_EOL.'Install the extension';
         }
-        $message .= ' or use a polyfill that provide';
+        $message .= ' or use a polyfill that provide ';
 
         $classesMessage = null;
         if (1 === count($classes)) {
-            $classesMessage = ' the class "'.reset($classes).'"';
+            $classesMessage = 'the class "'.reset($classes).'"';
         } elseif (count($classes) > 1) {
-            $classesMessage = ' the classes "'.implode('", "', $classes).'"';
+            $classesMessage = 'the classes "'.implode('", "', $classes).'"';
         }
         $methodsMessage = null;
         if (1 === count($methods)) {
-            $methodsMessage = ' the method "'.reset($methods).'"';
+            $methodsMessage = 'the method "'.reset($methods).'"';
         } elseif (count($methods) > 1) {
-            $methodsMessage = ' the methods "'.implode('", "', $methods).'"';
+            $methodsMessage = 'the methods "'.implode('", "', $methods).'"';
         }
         $functionsMessage = null;
         if (1 === count($functions)) {
-            $functionsMessage = ' the function "'.reset($functions).'"';
+            $functionsMessage = 'the function "'.reset($functions).'"';
         } elseif (count($functions) > 1) {
-            $functionsMessage = ' the functions "'.implode('", "', $functions).'"';
+            $functionsMessage = 'the functions "'.implode('", "', $functions).'"';
         }
 
         $message .= implode(' and ', array_filter([$classesMessage, $methodsMessage, $functionsMessage]));
+        $message .= '.';
 
         return $message;
     }
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
index 0989bb3..3494c85 100644
--- a/src/Redis/Client/AmpRedisClient.php
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -52,9 +52,6 @@ public static function make($redis): Client
         return new self($redis);
     }
 
-    /*[]
-     * @inheritDoc
-     */
     public function execute(Command $command)
     {
         $result = wait($this->redis->query($command->getId(), ...array_map('strval', $command->getArguments())));
@@ -77,7 +74,8 @@ public static function supports($redis): bool
 
     protected function doPipeline(Command ...$commands): array
     {
-        trigger_error('Warning, \\Amp\\Redis\\Redis don\'t use a real Redis Pipeline', E_USER_NOTICE);
+        false === static::$disableNotice
+            && trigger_error('Warning, \\Amp\\Redis\\Redis don\'t use a real Redis Pipeline', E_USER_NOTICE);
 
         return wait(all(
             array_map(function (Command $command) {
diff --git a/src/Redis/Client/CheprasovRedisClient.php b/src/Redis/Client/CheprasovRedisClient.php
index 3ddc522..2d1b66c 100644
--- a/src/Redis/Client/CheprasovRedisClient.php
+++ b/src/Redis/Client/CheprasovRedisClient.php
@@ -74,7 +74,8 @@ public static function supports($redis): bool
 
     protected function doPipeline(Command ...$commands): array
     {
-        trigger_error('Warning, a workaround is used to enable custom command in pipeline for \\RedisClient\\Pipeline\\PipelineInterface', E_USER_NOTICE);
+        false === static::$disableNotice
+            && trigger_error('Warning, a workaround is used to enable custom command in pipeline for \\RedisClient\\Pipeline\\PipelineInterface', E_USER_NOTICE);
 
         return $this->redis->pipeline(function (PipelineInterface $pipeline) use ($commands): void {
             foreach ($commands as $command) {
diff --git a/src/Redis/Client/CredisClient.php b/src/Redis/Client/CredisClient.php
index 4150224..8a8eeb5 100644
--- a/src/Redis/Client/CredisClient.php
+++ b/src/Redis/Client/CredisClient.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Client;
 
 use Credis_Client;
+use function is_array;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
@@ -38,7 +39,7 @@ private function __construct(Credis_Client $redis)
     {
         if (!self::supports($redis)) {
             throw new RuntimeException($this->getMissingMessage('Credis', false, [
-                Credis_Client::class => ['__call', 'pipeline', 'exec'],
+                Credis_Client::class => ['__call'],
             ]));
         }
         $this->redis = $redis;
@@ -53,22 +54,20 @@ public function execute(Command $command)
     {
         $result = $this->redis->__call($command->getId(), $command->getArguments());
 
-        return $command->parseResponse($result);
+        return $command->parseResponse($this->fixFalseToNull($result));
     }
 
     public function executeRaw(...$args)
     {
         $command = array_shift($args);
 
-        return $this->redis->__call($command, $args);
+        return $this->fixFalseToNull($this->redis->__call($command, $args));
     }
 
     public static function supports($redis): bool
     {
         return $redis instanceof Credis_Client
-            && method_exists($redis, '__call')
-            && method_exists($redis, 'pipeline')
-            && method_exists($redis, 'exec');
+            && method_exists($redis, '__call');
     }
 
     protected function doPipeline(Command ...$commands): array
@@ -78,6 +77,25 @@ protected function doPipeline(Command ...$commands): array
             $pipeline = $pipeline->__call($command->getId(), $command->getArguments());
         }
 
-        return $pipeline->exec();
+        return $this->fixFalseToNull($pipeline->exec());
+    }
+
+    /**
+     * @param false|mixed $response
+     *
+     * @return null|mixed
+     */
+    private function fixFalseToNull($response)
+    {
+        if (false === $response) {
+            return null;
+        }
+        if (is_array($response)) {
+            foreach ($response as &$item) {
+                $item = $this->fixFalseToNull($item);
+            }
+        }
+
+        return $response;
     }
 }
diff --git a/src/Redis/Client/TinyRedisClient.php b/src/Redis/Client/TinyRedisClient.php
new file mode 100644
index 0000000..3227064
--- /dev/null
+++ b/src/Redis/Client/TinyRedisClient.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client;
+
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+use RuntimeException;
+
+/**
+ * @codeCoverageIgnore
+ */
+class TinyRedisClient extends AbstractClient
+{
+    /** @var \TinyRedisClient */
+    private $redis;
+
+    public function __construct(\TinyRedisClient $redis)
+    {
+        if (!self::supports($redis)) {
+            throw new RuntimeException($this->getMissingMessage(
+                'TinyRedis',
+                false,
+                [\TinyRedisClient::class => ['__call']]
+            ));
+        }
+        $this->redis = $redis;
+    }
+
+    public function pipeline(Command ...$commands): array
+    {
+        false === static::$disableNotice
+            && trigger_error('Warning, \\TinyRedisClient don\'t use a real Redis Pipeline', E_USER_NOTICE);
+
+        return array_map(function (Command $command) {
+            return $this->execute($command);
+        }, $commands);
+    }
+
+    public function execute(Command $command)
+    {
+        $result = $this->redis->__call($command->getId(), $command->getArguments());
+
+        return $command->parseResponse($result);
+    }
+
+    public function executeRaw(...$args)
+    {
+        $command = array_shift($args);
+
+        return $this->redis->__call($command, $args);
+    }
+
+    public static function supports($redis): bool
+    {
+        return $redis instanceof \TinyRedisClient
+            && method_exists($redis, '__call');
+    }
+
+    public static function make($redis): Client
+    {
+        return new self($redis);
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        return [];
+    }
+}
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index d96337c..e5bb67b 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis;
 
 use function count;
+use MacFJA\RediSearch\Redis\Client\AbstractClient;
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
 use MacFJA\RediSearch\Redis\Command\AliasDel;
@@ -64,7 +65,7 @@ public function create(): Rediska_Connection_Exec
         {
             $connections = $this->_rediska->getConnections();
 
-            if (count($connections) > 1) {
+            if (false === AbstractClient::$disableNotice && count($connections) > 1) {
                 trigger_error('Warning, Multiple redis connections exists, only the first connection will be used', E_USER_NOTICE);
             }
             $connection = reset($connections);
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 78c64c2..d814ee2 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -24,6 +24,7 @@
 use Amp\Redis\Config;
 use Amp\Redis\RemoteExecutor;
 use Closure;
+use Credis_Client;
 use MacFJA\RediSearch\Index;
 use MacFJA\RediSearch\IndexBuilder;
 use MacFJA\RediSearch\Query\Builder;
@@ -39,8 +40,8 @@
 use MacFJA\RediSearch\Redis\Command\SynUpdate;
 use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
 use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\TestResult;
 use Rediska;
+use TinyRedisClient;
 
 /**
  * @covers \MacFJA\RediSearch\Index
@@ -104,6 +105,7 @@ class DockerTest extends TestCase
     public static function setUpBeforeClass(): void
     {
         parent::setUpBeforeClass();
+        Client\AbstractClient::$disableNotice = true;
         exec('which docker', $output, $code);
         if ($code > 0) {
             static::markTestSkipped('Docker is missing');
@@ -118,6 +120,7 @@ public static function tearDownAfterClass(): void
         foreach ($output as $container) {
             exec('docker stop '.escapeshellarg($container), $output);
         }
+        Client\AbstractClient::$disableNotice = false;
     }
 
     /**
@@ -129,11 +132,6 @@ public static function tearDownAfterClass(): void
     public function testIntegration($clientBuilder): void
     {
         $client = $clientBuilder();
-        $resultObject = $this->getTestResultObject();
-        if ($resultObject instanceof TestResult) {
-            $resultObject->beStrictAboutOutputDuringTests(false);
-            $resultObject->convertNoticesToExceptions(false);
-        }
 
         $list = $client->execute(new IndexList());
         static::assertEmpty($list);
@@ -223,6 +221,8 @@ public function dataProvider(): array
             [static function () { return Client\RedisentClient::make(new \redisent\Redis('redis://localhost:16379')); }],
             [static function () { return Client\CheprasovRedisClient::make(new \RedisClient\Client\Version\RedisClient6x0(['server' => 'localhost:16379', 'database' => 0])); }],
             [static function () { return Client\AmpRedisClient::make(new \Amp\Redis\Redis(new RemoteExecutor(Config::fromUri('redis://localhost:16379')))); }],
+            [static function () { return Client\TinyRedisClient::make(new TinyRedisClient('localhost:16379')); }],
+            [static function () { return Client\CredisClient::make(new Credis_Client('localhost', 16379, null, '', 0)); }],
         ];
     }
 }

From 81c1d7ff1c9aa77aeadc8b320094849be86bfaad Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 26 Oct 2021 20:35:43 +0200
Subject: [PATCH 17/60] Remove unnecessary 'transformParsedResponse' function

---
 .../UnexpectedServerResponseException.php     | 54 +++++++++++++++++++
 src/Redis/Command/AbstractCommand.php         | 26 ++++-----
 src/Redis/Command/Aggregate.php               | 25 ++++++---
 src/Redis/Command/CursorRead.php              | 23 +++++---
 src/Redis/Command/Info.php                    | 22 +++++---
 src/Redis/Command/Search.php                  | 12 ++---
 src/Redis/Command/SpellCheck.php              | 20 ++++---
 src/Redis/Command/SugGet.php                  | 15 +++---
 src/Redis/Response/CursorResponse.php         |  2 +-
 tests/Redis/Command/InfoTest.php              | 10 ++++
 tests/Redis/Command/SpellCheckTest.php        | 37 ++++++++++---
 tests/Redis/Command/SugGetTest.php            | 39 +++++++++++++-
 tests/Redis/Response/InfoResponseTest.php     |  1 -
 13 files changed, 212 insertions(+), 74 deletions(-)
 create mode 100644 src/Exception/UnexpectedServerResponseException.php

diff --git a/src/Exception/UnexpectedServerResponseException.php b/src/Exception/UnexpectedServerResponseException.php
new file mode 100644
index 0000000..7d0b9d8
--- /dev/null
+++ b/src/Exception/UnexpectedServerResponseException.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Exception;
+
+use function is_string;
+use UnexpectedValueException;
+
+class UnexpectedServerResponseException extends UnexpectedValueException
+{
+    /** @var mixed */
+    private $response;
+
+    /**
+     * @param mixed $response
+     */
+    public function __construct($response, ?string $message = null)
+    {
+        $this->response = $response;
+        $additional = '';
+        if (is_string($message)) {
+            $additional = ': '.$message;
+        }
+        parent::__construct('Unexpected response from the Redis server'.$additional);
+    }
+
+    /**
+     * @codeCoverageIgnore
+     *
+     * @return mixed
+     */
+    public function getResponse()
+    {
+        return $this->response;
+    }
+}
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index f3650d2..08d8dc9 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -63,16 +63,6 @@ public function setRediSearchVersion(string $rediSearchVersion): Command
         return $this;
     }
 
-    /**
-     * @param array<mixed>|float|int|mixed|string $data
-     *
-     * @return mixed|string
-     */
-    public function parseResponse($data)
-    {
-        return $this->transformParsedResponse($data);
-    }
-
     /**
      * @return array<float|int|string>
      */
@@ -99,20 +89,22 @@ public function getArguments(): array
     }
 
     /**
-     * @return array<string>
-     */
-    abstract protected function getRequiredOptions(): array;
-
-    /**
-     * @param mixed $data
+     * @codeCoverageIgnore
+     *
+     * @param array<mixed>|float|int|mixed|string $data
      *
      * @return mixed
      */
-    protected function transformParsedResponse($data)
+    public function parseResponse($data)
     {
         return $data;
     }
 
+    /**
+     * @return array<string>
+     */
+    abstract protected function getRequiredOptions(): array;
+
     /**
      * @return array<string>
      */
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index c3b530f..94e3432 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -22,6 +22,8 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function assert;
+use function is_array;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\LimitOption;
@@ -33,10 +35,11 @@
 use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
 use MacFJA\RediSearch\Redis\Response\AggregateResponseItem;
 use MacFJA\RediSearch\Redis\Response\ArrayResponseTrait;
+use MacFJA\RediSearch\Redis\Response\CursorResponse;
 use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
 
 /**
- * @method CursorResponse|PaginatedResponse<AggregateResponseItem> parseResponse(mixed $data)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  */
 class Aggregate extends AbstractCommand implements PaginatedCommand
 {
@@ -180,13 +183,18 @@ public function getSize(): ?int
         return $limit->getSize();
     }
 
-    protected function getRequiredOptions(): array
+    /**
+     * @param array|mixed|string $data
+     *
+     * @return CursorResponse|PaginatedResponse<AggregateResponseItem>
+     * @phpstan-return CursorResponse|PaginatedResponse
+     */
+    public function parseResponse($data)
     {
-        return ['index', 'query'];
-    }
+        if (!is_array($data)) {
+            throw new UnexpectedServerResponseException($data);
+        }
 
-    protected function transformParsedResponse($data)
-    {
         if (true === $this->options['cursor']->getDataOfOption('enabled')) {
             return CursorRead::transformResponse(
                 $data,
@@ -206,4 +214,9 @@ protected function transformParsedResponse($data)
 
         return new PaginatedResponse($this, $totalCount, $items);
     }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'query'];
+    }
 }
diff --git a/src/Redis/Command/CursorRead.php b/src/Redis/Command/CursorRead.php
index 4c553ac..a380166 100644
--- a/src/Redis/Command/CursorRead.php
+++ b/src/Redis/Command/CursorRead.php
@@ -25,6 +25,7 @@
 use function count;
 use function is_array;
 use function is_int;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
@@ -32,9 +33,6 @@
 use MacFJA\RediSearch\Redis\Response\ArrayResponseTrait;
 use MacFJA\RediSearch\Redis\Response\CursorResponse;
 
-/**
- * @method CursorResponse parseResponse(mixed $data)
- */
 class CursorRead extends AbstractCommand
 {
     use ArrayResponseTrait;
@@ -102,13 +100,17 @@ public static function transformResponse(array $data, ?int $count, string $index
         return new CursorResponse($cursorId, $totalCount, $items, $size, $index, $rediSearchVersion);
     }
 
-    protected function getRequiredOptions(): array
+    /**
+     * @param array|mixed $data
+     *
+     * @return CursorResponse
+     */
+    public function parseResponse($data)
     {
-        return ['type', 'index', 'cursor'];
-    }
+        if (!is_array($data)) {
+            throw new UnexpectedServerResponseException($data);
+        }
 
-    protected function transformParsedResponse($data)
-    {
         return self::transformResponse(
             $data,
             $this->options['count']->getValue(),
@@ -116,4 +118,9 @@ protected function transformParsedResponse($data)
             $this->getRediSearchVersion()
         );
     }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['type', 'index', 'cursor'];
+    }
 }
diff --git a/src/Redis/Command/Info.php b/src/Redis/Command/Info.php
index 8c0eb8e..50b4b42 100644
--- a/src/Redis/Command/Info.php
+++ b/src/Redis/Command/Info.php
@@ -21,12 +21,11 @@
 
 namespace MacFJA\RediSearch\Redis\Command;
 
+use function is_array;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Response\InfoResponse;
 
-/**
- * @method InfoResponse parseResponse(mixed $data)
- */
 class Info extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -48,13 +47,22 @@ public function getId(): string
         return 'FT.INFO';
     }
 
-    protected function getRequiredOptions(): array
+    /**
+     * @param array|mixed $data
+     *
+     * @return InfoResponse
+     */
+    public function parseResponse($data)
     {
-        return ['index'];
+        if (!is_array($data)) {
+            throw new UnexpectedServerResponseException($data);
+        }
+
+        return new InfoResponse($data);
     }
 
-    protected function transformParsedResponse($data)
+    protected function getRequiredOptions(): array
     {
-        return new InfoResponse($data);
+        return ['index'];
     }
 }
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 3dddb3c..1d86c48 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -23,8 +23,8 @@
 
 use function assert;
 use function count;
-use InvalidArgumentException;
 use function is_array;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
@@ -302,15 +302,15 @@ public function getSize(): ?int
     /**
      * @param mixed $data
      *
-     * @return mixed|PaginatedResponse
+     * @return PaginatedResponse
      *
      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
      * @SuppressWarnings(PHPMD.NPathComplexity)
      */
-    public function transformParsedResponse($data)
+    public function parseResponse($data)
     {
         if (!is_array($data)) {
-            return $data;
+            throw new UnexpectedServerResponseException($data);
         }
 
         $totalCount = array_shift($data);
@@ -329,7 +329,7 @@ public function transformParsedResponse($data)
 
         $documents = array_chunk($data, $chunkSize);
 
-        $items = array_map(static function ($document) use ($useSortKeys, $usePayloads, $useScores, $noContent) {
+        $items = array_map(static function ($document) use ($data, $useSortKeys, $usePayloads, $useScores, $noContent) {
             $hash = array_shift($document) ?? '';
             $score = true === $useScores ? (float) array_shift($document) : null;
             $payload = true === $usePayloads ? array_shift($document) : null;
@@ -338,7 +338,7 @@ public function transformParsedResponse($data)
             $fields = [];
             if (false === $noContent) {
                 if (!(1 === count($document))) {
-                    throw new InvalidArgumentException();
+                    throw new UnexpectedServerResponseException($data, 'Incomplete response');
                 }
                 $rawData = reset($document);
                 assert(is_array($rawData));
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
index 06d1850..b2a196d 100644
--- a/src/Redis/Command/SpellCheck.php
+++ b/src/Redis/Command/SpellCheck.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function is_array;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
@@ -29,9 +30,6 @@
 use MacFJA\RediSearch\Redis\Response\SpellCheckResponseItem;
 use Respect\Validation\Validator;
 
-/**
- * @method array<SpellCheckResponseItem> parseResponse(mixed $data)
- */
 class SpellCheck extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -83,20 +81,15 @@ public function getId(): string
         return 'FT.SPELLCHECK';
     }
 
-    protected function getRequiredOptions(): array
-    {
-        return ['index', 'query'];
-    }
-
     /**
      * @param mixed $data
      *
-     * @return mixed|SpellCheckResponseItem[]
+     * @return SpellCheckResponseItem[]
      */
-    protected function transformParsedResponse($data)
+    public function parseResponse($data)
     {
         if (!is_array($data)) {
-            return $data;
+            throw new UnexpectedServerResponseException($data);
         }
 
         $items = array_map(static function (array $group) {
@@ -113,4 +106,9 @@ protected function transformParsedResponse($data)
 
         return array_filter($items);
     }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'query'];
+    }
 }
diff --git a/src/Redis/Command/SugGet.php b/src/Redis/Command/SugGet.php
index fd0a71d..5d260f6 100644
--- a/src/Redis/Command/SugGet.php
+++ b/src/Redis/Command/SugGet.php
@@ -22,17 +22,14 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function count;
-use InvalidArgumentException;
 use function is_array;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Response\SuggestionResponseItem;
 
-/**
- * @method array<SuggestionResponseItem> parseResponse(mixed $data)
- */
 class SugGet extends AbstractCommand
 {
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
@@ -100,12 +97,12 @@ public function getId(): string
     /**
      * @param mixed $data
      *
-     * @return mixed|SuggestionResponseItem[]
+     * @return SuggestionResponseItem[]
      */
-    public function transformParsedResponse($data)
+    public function parseResponse($data)
     {
         if (!is_array($data)) {
-            return $data;
+            throw new UnexpectedServerResponseException($data);
         }
 
         $useScores = $this->options['scores']->isActive();
@@ -117,12 +114,12 @@ public function transformParsedResponse($data)
 
         $documents = array_chunk($data, $chunkSize);
 
-        return array_map(static function ($document) use ($usePayloads, $useScores) {
+        return array_map(static function ($document) use ($data, $usePayloads, $useScores) {
             $payload = true === $usePayloads ? array_pop($document) : null;
             $score = true === $useScores ? (float) array_pop($document) : null;
 
             if (!(1 === count($document))) {
-                throw new InvalidArgumentException();
+                throw new UnexpectedServerResponseException($data, 'Incomplete response');
             }
             $value = reset($document);
 
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index dd1d9a4..b81f052 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -73,7 +73,7 @@ public function setClient(Client $client): self
     }
 
     /**
-     * @return array<AggregateResponseItem>|array<SearchResponseItem>
+     * @return array<AggregateResponseItem>
      */
     public function current()
     {
diff --git a/tests/Redis/Command/InfoTest.php b/tests/Redis/Command/InfoTest.php
index e28ccac..d1b9b36 100644
--- a/tests/Redis/Command/InfoTest.php
+++ b/tests/Redis/Command/InfoTest.php
@@ -21,10 +21,13 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Info;
 use PHPUnit\Framework\TestCase;
 
 /**
+ * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
+ *
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
  *
  * @covers \MacFJA\RediSearch\Redis\Command\Info
@@ -53,4 +56,11 @@ public function testFullOption(): void
             'idx',
         ], $command->getArguments());
     }
+
+    public function testParseResponseInvalid(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+        $command = new Info();
+        $command->parseResponse('foo');
+    }
 }
diff --git a/tests/Redis/Command/SpellCheckTest.php b/tests/Redis/Command/SpellCheckTest.php
index ffa86ca..5ae27b3 100644
--- a/tests/Redis/Command/SpellCheckTest.php
+++ b/tests/Redis/Command/SpellCheckTest.php
@@ -21,12 +21,14 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
-use Closure;
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\SpellCheck;
 use MacFJA\RediSearch\Redis\Response\SpellCheckResponseItem;
 use PHPUnit\Framework\TestCase;
 
 /**
+ * @covers  \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
+ *
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
  *
  * @covers \MacFJA\RediSearch\Redis\Command\SpellCheck
@@ -67,7 +69,7 @@ public function testFullOption(): void
         ], $command->getArguments());
     }
 
-    public function testResponseTransformation(): void
+    public function testParseResponse(): void
     {
         $command = new SpellCheck();
         $expected = [
@@ -78,11 +80,32 @@ public function testResponseTransformation(): void
             ['TERM', 'hell', [[0.7, 'hello'], [0.4, 'hola']]],
             ['TERM', 'worl', [[0.9, 'world'], [0.56, 'work']]],
         ];
-        $transformer = function ($data) {
-            // @phpstan-ignore-next-line
-            return $this->transformParsedResponse($data);
-        };
-        $actual = Closure::fromCallable($transformer)->call($command, $rawResponse);
+
+        $actual = $command->parseResponse($rawResponse);
+
+        static::assertEquals($expected, $actual);
+    }
+
+    public function testParseResponseInvalid(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+        $command = new SpellCheck();
+
+        $command->parseResponse('foobar');
+    }
+
+    public function testParseResponsePartialInvalid(): void
+    {
+        $command = new SpellCheck();
+        $expected = [
+            new SpellCheckResponseItem('hell', ['hello' => 0.7, 'hola' => 0.4]),
+        ];
+        $rawResponse = [
+            ['TERM', 'hell', [[0.7, 'hello'], [0.4, 'hola']]],
+            ['TERMbar', 'worl', [[0.9, 'world'], [0.56, 'work']]],
+        ];
+
+        $actual = $command->parseResponse($rawResponse);
 
         static::assertEquals($expected, $actual);
     }
diff --git a/tests/Redis/Command/SugGetTest.php b/tests/Redis/Command/SugGetTest.php
index 84c64b8..b6a8acd 100644
--- a/tests/Redis/Command/SugGetTest.php
+++ b/tests/Redis/Command/SugGetTest.php
@@ -21,12 +21,15 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\SugGet;
+use MacFJA\RediSearch\Redis\Response\SuggestionResponseItem;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
  *
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\SugGet
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
@@ -61,4 +64,38 @@ public function testFullOption(): void
             'ac', 'hell', 'FUZZY', 'WITHSCORES', 'WITHPAYLOADS', 'MAX', 10,
         ], $command->getArguments());
     }
+
+    public function testParseResponse(): void
+    {
+        $expected = [
+            new SuggestionResponseItem('hello', 0.4024922251701355, 'greeting'),
+            new SuggestionResponseItem('hola', 0.40000000596046448, 'greeting'),
+        ];
+
+        $rawResult = [
+            'hello',  '0.4024922251701355', 'greeting',
+            'hola', '0.40000000596046448', 'greeting',
+        ];
+
+        $command = new SugGet();
+        $command->setWithPayloads()->setWithScores();
+
+        static::assertEquals($expected, $command->parseResponse($rawResult));
+    }
+
+    public function testInvalidServerResponse1(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+        $command = new SugGet();
+        $command->parseResponse('foobar');
+    }
+
+    public function testInvalidServerResponse2(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+        $this->expectExceptionMessage('Unexpected response from the Redis server: Incomplete response');
+        $command = new SugGet();
+        $command->setWithPayloads();
+        $command->parseResponse(['foobar']);
+    }
 }
diff --git a/tests/Redis/Response/InfoResponseTest.php b/tests/Redis/Response/InfoResponseTest.php
index 61899e8..356e91a 100644
--- a/tests/Redis/Response/InfoResponseTest.php
+++ b/tests/Redis/Response/InfoResponseTest.php
@@ -30,7 +30,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand::parseResponse
- * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand::transformParsedResponse
  * @covers \MacFJA\RediSearch\Redis\Command\Info
  * @covers \MacFJA\RediSearch\Redis\Response\InfoResponse
  *

From 71a8bdccb009faf43dcef7f61a34780ad666f218 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 30 Oct 2021 19:17:54 +0200
Subject: [PATCH 18/60] Update Changelog

---
 CHANGELOG.md | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ace7a09..7a6e044 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Added
+
+- Allow multiple connector for Redis ([PR#17])
+- Allow specific feature of RediSearch depending of its version ([Issue#6])
+- Add more language ([Issue#5])
+- Add more language ([Issue#5])
+- Add `SORTABLE` flag on `GEO` field ([Issue#5])
+- Add `CASESENSITIVE` flag on fields ([Issue#5])
+- Add `UNF` flag on fields ([Issue#5])
+
+### Changed
+
+- Remove hard dependency on Predis ([Issue#16])
+- Full rework of the library
+
 ## [Unreleased v1.x]
 
 ## [1.4.0]
@@ -106,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.x...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/main...HEAD
 [Unreleased v1.x]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...HEAD
 [1.4.0]: https://github.com/MacFJA/php-redisearch/compare/1.3.0...1.4.0
 [1.3.0]: https://github.com/MacFJA/php-redisearch/compare/1.2.0...1.3.0
@@ -115,13 +130,17 @@ First version
 [1.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/1.0.0
 
 [Issue#2]: https://github.com/MacFJA/php-redisearch/issues/2
+[Issue#5]: https://github.com/MacFJA/php-redisearch/issues/5
+[Issue#6]: https://github.com/MacFJA/php-redisearch/issues/6
 [Issue#9]: https://github.com/MacFJA/php-redisearch/issues/9
 [Issue#10]: https://github.com/MacFJA/php-redisearch/issues/10
 [Issue#11]: https://github.com/MacFJA/php-redisearch/issues/11
 [Issue#12]: https://github.com/MacFJA/php-redisearch/issues/12
+[Issue#16]: https://github.com/MacFJA/php-redisearch/issues/16
 [PR#1]: https://github.com/MacFJA/php-redisearch/pull/1
 [PR#3]: https://github.com/MacFJA/php-redisearch/pull/3
 [PR#8]: https://github.com/MacFJA/php-redisearch/pull/8
 [PR#13]: https://github.com/MacFJA/php-redisearch/pull/13
 [PR#14]: https://github.com/MacFJA/php-redisearch/pull/14
 [PR#15]: https://github.com/MacFJA/php-redisearch/pull/15
+[PR#17]: https://github.com/MacFJA/php-redisearch/pull/17

From 8ebffb438b81d7d475fb857d2cd94fa53c489689 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Thu, 4 Nov 2021 19:59:41 +0100
Subject: [PATCH 19/60] Improve client error message + add missing factory in
 facade + more tests

---
 Makefile                                      | 15 +++-
 src/Redis/Client/AbstractClient.php           | 10 ++-
 src/Redis/Client/AmpRedisClient.php           |  3 -
 src/Redis/Client/CheprasovRedisClient.php     |  3 -
 src/Redis/Client/ClientFacade.php             |  4 +-
 src/Redis/Client/CredisClient.php             |  3 -
 src/Redis/Client/PredisClient.php             |  3 -
 src/Redis/Client/RedisentClient.php           |  3 -
 src/Redis/Client/RediskaClient.php            |  3 -
 src/Redis/Client/TinyRedisClient.php          |  6 +-
 src/Redis/Response/PaginatedResponse.php      |  2 +-
 tests/Redis/Client/AbstractClientTest.php     | 63 +++++++++++++
 tests/Redis/Client/ClientFacadeTest.php       | 89 +++++++++++++++++++
 .../Redis/Client/FakeAbstractClient.php       | 62 +++++++++++++
 tests/integration/DockerTest.php              | 80 ++++++++++++++---
 15 files changed, 308 insertions(+), 41 deletions(-)
 create mode 100644 tests/Redis/Client/AbstractClientTest.php
 create mode 100644 tests/Redis/Client/ClientFacadeTest.php
 create mode 100644 tests/fixtures/Redis/Client/FakeAbstractClient.php

diff --git a/Makefile b/Makefile
index 7d54984..e9185a6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: analyze fix-code test coverage validation integration-test
+.PHONY: analyze fix-code test test-with-integration coverage coverage-with-integration validation integration-test integration-coverage
 
 analyze: | vendor
 	$(COMPOSER) exec -v parallel-lint -- src
@@ -18,14 +18,25 @@ fix-code: | vendor
 test: | vendor
 	$(COMPOSER) exec -v phpunit
 
+test-with-integration: | vendor
+	$(COMPOSER) exec -v phpunit -- --group default,integration
+
 coverage: | vendor
 	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
 	$(COMPOSER) exec -v phpunit -- --coverage-text --color
 
+coverage-with-integration: | vendor
+	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
+	$(COMPOSER) exec -v phpunit -- --group default,integration --coverage-text --color
+
 integration-test: | vendor
 	$(COMPOSER) exec -v phpunit -- --group integration
 
-validation: fix-code analyze test coverage integration-test
+integration-coverage: | vendor
+	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
+	$(COMPOSER) exec -v phpunit -- --group integration --coverage-text --color
+
+validation: fix-code analyze test-with-integration coverage-with-integration
 
 vendor: composer.json
 	$(COMPOSER) install --optimize-autoloader --no-suggest --prefer-dist
diff --git a/src/Redis/Client/AbstractClient.php b/src/Redis/Client/AbstractClient.php
index 470ac6b..193d9ed 100644
--- a/src/Redis/Client/AbstractClient.php
+++ b/src/Redis/Client/AbstractClient.php
@@ -24,6 +24,7 @@
 use function count;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
+use function strlen;
 
 abstract class AbstractClient implements Client
 {
@@ -71,7 +72,7 @@ protected function getMissingMessage(string $name, bool $isExtension, array $cla
         if (true === $isExtension) {
             $message = 'The extension '.$name.' is missing.'.PHP_EOL.'Install the extension';
         }
-        $message .= ' or use a polyfill that provide ';
+        $message .= ' or use a polyfill';
 
         $classesMessage = null;
         if (1 === count($classes)) {
@@ -92,8 +93,11 @@ protected function getMissingMessage(string $name, bool $isExtension, array $cla
             $functionsMessage = 'the functions "'.implode('", "', $functions).'"';
         }
 
-        $message .= implode(' and ', array_filter([$classesMessage, $methodsMessage, $functionsMessage]));
-        $message .= '.';
+        $additional = implode(' and ', array_filter([$classesMessage, $methodsMessage, $functionsMessage]));
+        if (strlen($additional) > 0) {
+            $additional = ' that provide '.$additional;
+        }
+        $message .= $additional.'.';
 
         return $message;
     }
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
index 3494c85..3a83d1d 100644
--- a/src/Redis/Client/AmpRedisClient.php
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -29,9 +29,6 @@
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class AmpRedisClient extends AbstractClient
 {
     /** @var Redis */
diff --git a/src/Redis/Client/CheprasovRedisClient.php b/src/Redis/Client/CheprasovRedisClient.php
index 2d1b66c..60d11c8 100644
--- a/src/Redis/Client/CheprasovRedisClient.php
+++ b/src/Redis/Client/CheprasovRedisClient.php
@@ -28,9 +28,6 @@
 use RedisClient\Pipeline\PipelineInterface;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class CheprasovRedisClient extends AbstractClient
 {
     /** @var AbstractRedisClient */
diff --git a/src/Redis/Client/ClientFacade.php b/src/Redis/Client/ClientFacade.php
index b0328bd..b213c46 100644
--- a/src/Redis/Client/ClientFacade.php
+++ b/src/Redis/Client/ClientFacade.php
@@ -31,9 +31,6 @@
 use Rediska as RediskaRedis;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class ClientFacade
 {
     /** @var array<string> */
@@ -46,6 +43,7 @@ class ClientFacade
         RedisentClient::class,
         RediskaClient::class,
         AmpRedisClient::class,
+        TinyRedisClient::class,
     ];
 
     /**
diff --git a/src/Redis/Client/CredisClient.php b/src/Redis/Client/CredisClient.php
index 8a8eeb5..f7fd35a 100644
--- a/src/Redis/Client/CredisClient.php
+++ b/src/Redis/Client/CredisClient.php
@@ -27,9 +27,6 @@
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class CredisClient extends AbstractClient
 {
     /** @var Credis_Client */
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
index b1c66d1..aca9e71 100644
--- a/src/Redis/Client/PredisClient.php
+++ b/src/Redis/Client/PredisClient.php
@@ -29,9 +29,6 @@
 use Predis\Command\RawCommand;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class PredisClient extends AbstractClient
 {
     /** @var ClientInterface */
diff --git a/src/Redis/Client/RedisentClient.php b/src/Redis/Client/RedisentClient.php
index 1d1b46e..5adc2a4 100644
--- a/src/Redis/Client/RedisentClient.php
+++ b/src/Redis/Client/RedisentClient.php
@@ -26,9 +26,6 @@
 use redisent\Redis;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class RedisentClient extends AbstractClient
 {
     /** @var Redis */
diff --git a/src/Redis/Client/RediskaClient.php b/src/Redis/Client/RediskaClient.php
index ea867ab..b885e4a 100644
--- a/src/Redis/Client/RediskaClient.php
+++ b/src/Redis/Client/RediskaClient.php
@@ -31,9 +31,6 @@
 use Rediska_Connection_Exec;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class RediskaClient extends AbstractClient
 {
     /** @var Rediska */
diff --git a/src/Redis/Client/TinyRedisClient.php b/src/Redis/Client/TinyRedisClient.php
index 3227064..1055783 100644
--- a/src/Redis/Client/TinyRedisClient.php
+++ b/src/Redis/Client/TinyRedisClient.php
@@ -25,9 +25,6 @@
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
 
-/**
- * @codeCoverageIgnore
- */
 class TinyRedisClient extends AbstractClient
 {
     /** @var \TinyRedisClient */
@@ -80,6 +77,9 @@ public static function make($redis): Client
         return new self($redis);
     }
 
+    /**
+     * @codeCoverageIgnore
+     */
     protected function doPipeline(Command ...$commands): array
     {
         return [];
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index f800850..b3d38f0 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -128,7 +128,7 @@ public function getTotalCount(): int
 
     public function count()
     {
-        return $this->totalCount;
+        return $this->getTotalCount();
     }
 
     private function updateWithLimit(int $offset, int $size): void
diff --git a/tests/Redis/Client/AbstractClientTest.php b/tests/Redis/Client/AbstractClientTest.php
new file mode 100644
index 0000000..c4989df
--- /dev/null
+++ b/tests/Redis/Client/AbstractClientTest.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Client;
+
+use MacFJA\RediSearch\tests\fixtures\Redis\Client\FakeAbstractClient;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Client\AbstractClient
+ *
+ * @internal
+ */
+class AbstractClientTest extends TestCase
+{
+    public function testGetMissingMessage(): void
+    {
+        $fake = new FakeAbstractClient();
+
+        static::assertSame(
+            'The dependency foo is missing.'.PHP_EOL.'Install the dependency or use a polyfill.',
+            $fake->exposeGetMissingMessage('foo', false, [])
+        );
+
+        static::assertSame(
+            'The dependency bar is missing.'.PHP_EOL.'Install the dependency or use a polyfill that provide the class "baz" and the method "baz::hello".',
+            $fake->exposeGetMissingMessage('bar', false, ['baz' => ['hello']])
+        );
+
+        static::assertSame(
+            'The dependency baz is missing.'.PHP_EOL.'Install the dependency or use a polyfill that provide the function "foobar".',
+            $fake->exposeGetMissingMessage('baz', false, [], ['foobar'])
+        );
+
+        static::assertSame(
+            'The dependency DEP is missing.'.PHP_EOL.'Install the dependency or use a polyfill that provide the classes "C", "CL" and the methods "C::m", "C::e", "CL::t", "CL::h" and the functions "f", "u".',
+            $fake->exposeGetMissingMessage('DEP', false, ['C' => ['m', 'e'], 'CL' => ['t', 'h']], ['f', 'u'])
+        );
+
+        static::assertSame(
+            'The extension foobar is missing.'.PHP_EOL.'Install the extension or use a polyfill.',
+            $fake->exposeGetMissingMessage('foobar', true, [])
+        );
+    }
+}
diff --git a/tests/Redis/Client/ClientFacadeTest.php b/tests/Redis/Client/ClientFacadeTest.php
new file mode 100644
index 0000000..054096d
--- /dev/null
+++ b/tests/Redis/Client/ClientFacadeTest.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Client;
+
+use function get_class;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Client\ClientFacade;
+use MacFJA\RediSearch\Redis\Command;
+use PHPUnit\Framework\TestCase;
+use RuntimeException;
+use stdClass;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Client\ClientFacade
+ *
+ * @uses \MacFJA\RediSearch\Redis\Client\AbstractClient
+ * @uses \MacFJA\RediSearch\Redis\Client\AmpRedisClient
+ * @uses \MacFJA\RediSearch\Redis\Client\CheprasovRedisClient
+ * @uses \MacFJA\RediSearch\Redis\Client\PredisClient
+ * @uses \MacFJA\RediSearch\Redis\Client\RedisentClient
+ * @uses \MacFJA\RediSearch\Redis\Client\RediskaClient
+ * @uses \MacFJA\RediSearch\Redis\Client\CredisClient
+ * @uses \MacFJA\RediSearch\Redis\Client\TinyRedisClient
+ *
+ * @internal
+ */
+class ClientFacadeTest extends TestCase
+{
+    public function testGetClientError(): void
+    {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessage('Unable to handle the Redis connection');
+
+        $facade = new ClientFacade();
+        $facade->getClient(new stdClass());
+    }
+
+    public function testAddFactory(): void
+    {
+        $facade = new ClientFacade();
+        static::assertFalse($facade->addFactory('Foobar'));
+
+        static::assertTrue($facade->addFactory(get_class(new class() implements Client {
+            public static function make($redis): Client
+            {
+                return new self();
+            }
+
+            public function execute(Command $command): void
+            {
+                // Do nothing
+            }
+
+            public function executeRaw(...$args): void
+            {
+                // Do nothing
+            }
+
+            public function pipeline(Command ...$commands): array
+            {
+                return [];
+            }
+
+            public static function supports($redis): bool
+            {
+                return false;
+            }
+        })));
+    }
+}
diff --git a/tests/fixtures/Redis/Client/FakeAbstractClient.php b/tests/fixtures/Redis/Client/FakeAbstractClient.php
new file mode 100644
index 0000000..179229d
--- /dev/null
+++ b/tests/fixtures/Redis/Client/FakeAbstractClient.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Client;
+
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command;
+
+class FakeAbstractClient extends \MacFJA\RediSearch\Redis\Client\AbstractClient
+{
+    public static function make($redis): Client
+    {
+        return new self();
+    }
+
+    public function execute(Command $command): void
+    {
+        // Do nothing
+    }
+
+    public function executeRaw(...$args): void
+    {
+        // Do nothing
+    }
+
+    public static function supports($redis): bool
+    {
+        return false;
+    }
+
+    /**
+     * @param array<string,array<string>> $classesMethods
+     * @param array<string>               $functions
+     */
+    public function exposeGetMissingMessage(string $name, bool $isExtension, array $classesMethods, array $functions = []): string
+    {
+        return $this->getMissingMessage($name, $isExtension, $classesMethods, $functions);
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        return [];
+    }
+}
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index d814ee2..1333b71 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -25,11 +25,13 @@
 use Amp\Redis\RemoteExecutor;
 use Closure;
 use Credis_Client;
+use function get_class;
 use MacFJA\RediSearch\Index;
 use MacFJA\RediSearch\IndexBuilder;
 use MacFJA\RediSearch\Query\Builder;
 use MacFJA\RediSearch\Query\Builder\Fuzzy;
 use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Client\ClientFacade;
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
@@ -39,6 +41,7 @@
 use MacFJA\RediSearch\Redis\Command\SynDump;
 use MacFJA\RediSearch\Redis\Command\SynUpdate;
 use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
+use MacFJA\RediSearch\Redis\Response\SearchResponseItem;
 use PHPUnit\Framework\TestCase;
 use Rediska;
 use TinyRedisClient;
@@ -50,9 +53,12 @@
  * @covers \MacFJA\RediSearch\Redis\Client\AbstractClient
  * @covers \MacFJA\RediSearch\Redis\Client\AmpRedisClient
  * @covers \MacFJA\RediSearch\Redis\Client\CheprasovRedisClient
+ * @covers \MacFJA\RediSearch\Redis\Client\ClientFacade
+ * @covers \MacFJA\RediSearch\Redis\Client\CredisClient
  * @covers \MacFJA\RediSearch\Redis\Client\PredisClient
  * @covers \MacFJA\RediSearch\Redis\Client\RedisentClient
  * @covers \MacFJA\RediSearch\Redis\Client\RediskaClient
+ * @covers \MacFJA\RediSearch\Redis\Client\TinyRedisClient
  * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
  * @covers \MacFJA\RediSearch\Redis\Command\DropIndex
  * @covers \MacFJA\RediSearch\Redis\Command\IndexList
@@ -102,20 +108,28 @@
  */
 class DockerTest extends TestCase
 {
+    /** @var bool */
+    private static $skip = false;
+
     public static function setUpBeforeClass(): void
     {
         parent::setUpBeforeClass();
-        Client\AbstractClient::$disableNotice = true;
         exec('which docker', $output, $code);
         if ($code > 0) {
-            static::markTestSkipped('Docker is missing');
+            static::$skip = true;
+
+            return;
         }
         exec('docker run --rm -p 16379:6379 -d redislabs/redisearch:latest');
+        Client\AbstractClient::$disableNotice = true;
     }
 
     public static function tearDownAfterClass(): void
     {
         parent::tearDownAfterClass();
+        if (static::$skip) {
+            return;
+        }
         exec('docker ps --filter publish=16379 -q', $output);
         foreach ($output as $container) {
             exec('docker stop '.escapeshellarg($container), $output);
@@ -123,6 +137,29 @@ public static function tearDownAfterClass(): void
         Client\AbstractClient::$disableNotice = false;
     }
 
+    protected function setUp(): void
+    {
+        parent::setUp();
+        if (static::$skip) {
+            static::markTestSkipped('Docker is missing');
+        }
+    }
+
+    /**
+     * @param mixed $redisBuilder
+     *
+     * @medium
+     *
+     * @dataProvider dataProvider
+     */
+    public function testGetClient(string $expectedClass, $redisBuilder): void
+    {
+        $facade = new ClientFacade();
+        $client = $facade->getClient($redisBuilder());
+
+        static::assertSame($expectedClass, get_class($client));
+    }
+
     /**
      * @dataProvider dataProvider
      * @large
@@ -168,6 +205,15 @@ public function testIntegration($clientBuilder): void
         /** @var PaginatedResponse $result */
         $result = $client->execute($search);
         static::assertCount(3, $result);
+        static::assertSame(1, $result->getPageCount());
+
+        /** @var array<SearchResponseItem> $page */
+        foreach ($result as $page) {
+            foreach ($page as $item) {
+                static::assertSame(30, (int) $item->getFieldValue('age'));
+            }
+        }
+
         $client->execute((new SynUpdate())->setIndex('testDoc')->setGroupId('Joe')->setTerms('John'));
         $syns = $client->execute((new SynDump())->setIndex('testDoc'));
         static::assertEquals([
@@ -211,18 +257,30 @@ public function testIntegration($clientBuilder): void
     }
 
     /**
-     * @return Closure[][]
+     * @return array<array<Closure|string>>
      */
-    public function dataProvider(): array
+    public function dataProvider(string $testName): array
     {
+        if ('testIntegration' === $testName) {
+            return [
+                [static function () { return Client\PredisClient::make(new \Predis\Client(['scheme' => 'tcp', 'host' => 'localhost', 'port' => '16379', 'db' => 0])); }],
+                [static function () { return Client\RediskaClient::make(new Rediska(['servers' => [['host' => 'localhost', 'port' => '16379', 'db' => 0]]])); }],
+                [static function () { return Client\RedisentClient::make(new \redisent\Redis('redis://localhost:16379')); }],
+                [static function () { return Client\CheprasovRedisClient::make(new \RedisClient\Client\Version\RedisClient6x0(['server' => 'localhost:16379', 'database' => 0])); }],
+                [static function () { return Client\AmpRedisClient::make(new \Amp\Redis\Redis(new RemoteExecutor(Config::fromUri('redis://localhost:16379')))); }],
+                [static function () { return Client\TinyRedisClient::make(new TinyRedisClient('localhost:16379')); }],
+                [static function () { return Client\CredisClient::make(new Credis_Client('localhost', 16379, null, '', 0)); }],
+            ];
+        }
+
         return [
-            [static function () { return Client\PredisClient::make(new \Predis\Client(['scheme' => 'tcp', 'host' => 'localhost', 'port' => '16379', 'db' => 0])); }],
-            [static function () { return Client\RediskaClient::make(new Rediska(['servers' => [['host' => 'localhost', 'port' => '16379', 'db' => 0]]])); }],
-            [static function () { return Client\RedisentClient::make(new \redisent\Redis('redis://localhost:16379')); }],
-            [static function () { return Client\CheprasovRedisClient::make(new \RedisClient\Client\Version\RedisClient6x0(['server' => 'localhost:16379', 'database' => 0])); }],
-            [static function () { return Client\AmpRedisClient::make(new \Amp\Redis\Redis(new RemoteExecutor(Config::fromUri('redis://localhost:16379')))); }],
-            [static function () { return Client\TinyRedisClient::make(new TinyRedisClient('localhost:16379')); }],
-            [static function () { return Client\CredisClient::make(new Credis_Client('localhost', 16379, null, '', 0)); }],
+            [Client\PredisClient::class, static function () { return new \Predis\Client(['scheme' => 'tcp', 'host' => 'localhost', 'port' => '16379', 'db' => 0]); }],
+            [Client\RediskaClient::class, static function () { return new Rediska(['servers' => [['host' => 'localhost', 'port' => '16379', 'db' => 0]]]); }],
+            [Client\RedisentClient::class, static function () { return new \redisent\Redis('redis://localhost:16379'); }],
+            [Client\CheprasovRedisClient::class, static function () { return new \RedisClient\Client\Version\RedisClient6x0(['server' => 'localhost:16379', 'database' => 0]); }],
+            [Client\AmpRedisClient::class, static function () { return new \Amp\Redis\Redis(new RemoteExecutor(Config::fromUri('redis://localhost:16379'))); }],
+            [Client\TinyRedisClient::class, static function () { return new TinyRedisClient('localhost:16379'); }],
+            [Client\CredisClient::class, static function () { return new Credis_Client('localhost', 16379, null, '', 0); }],
         ];
     }
 }

From 6a4f8326a88ca5f4d7e6337b899ce271c691731c Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 6 Nov 2021 19:14:05 +0100
Subject: [PATCH 20/60] Fix IndexBuilder + Improve README + More tests

---
 README.md                                     |  20 +-
 src/IndexBuilder.php                          |  27 +-
 .../Rediska/RediskaRediSearchCommand.php      |  47 +++
 src/Redis/Initializer.php                     |  86 ++---
 tests/IndexBuilderTest.php                    | 358 ++++++++++++++++++
 tests/integration/DockerTest.php              |   2 +-
 6 files changed, 462 insertions(+), 78 deletions(-)
 create mode 100644 src/Redis/Client/Rediska/RediskaRediSearchCommand.php
 create mode 100644 tests/IndexBuilderTest.php

diff --git a/README.md b/README.md
index 84ae2e8..ce03001 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [MacFJA/redisearch](https://packagist.org/packages/macfja/redisearch) is a PHP Client for [RediSearch](https://oss.redislabs.com/redisearch/).
 
-The implemented API is for RediSearch 2.0
+The implemented API is for RediSearch 2.x
 
 ## Installation
 
@@ -25,7 +25,7 @@ This lib can use several connector for Redis:
  - [Redisent](https://github.com/jdp/redisent) - Pure PHP implementation
  - [TinyRedis](https://github.com/ptrofimov/tinyredisclient) - Pure PHP implementation
 
-You can pick the connector depending of your need.
+You can pick the connector depending on your need.
 
 ```php
 $clientFacade = new \MacFJA\RediSearch\Redis\Client\ClientFacade();
@@ -70,14 +70,14 @@ $clientFacade->addFactory(\MyVendor\MyPackage\MyRedisClient::class);
 $client = /* ... */;
 $builder = new \MacFJA\RediSearch\IndexBuilder();
 
-// Field can be create in advance
+// Field can be created in advance
 $address = (new \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption())
     ->setField('address');
 
 $builder
     ->setIndex('person')
     ->addField($address)
-    // Or field can be create "inline"
+    // Or field can be created "inline"
     ->addTextField('lastname', false, null, null, true)
     ->addTextField('firstname')
     ->addNumericField('age')
@@ -186,7 +186,17 @@ $result = $client->pipeline($search, $stats, $aggregate, $suggestion);
 
 ```php
 $client = new \Predis\Client(/* ... */);
-\MacFJA\RediSearch\Redis\Initializer::registerCommands($client->getProfile());
+\MacFJA\RediSearch\Redis\Initializer::registerCommandsPredis($client->getProfile());
+
+$client->ftsearch('people', '@age:[(17 +inf] %john%');
+// But you will have raw Redis output.
+```
+
+### Use Rediska shorthand syntax
+
+```php
+$client = new \Rediska(/* ... */);
+\MacFJA\RediSearch\Redis\Initializer::registerCommandsRediska();
 
 $client->ftsearch('people', '@age:[(17 +inf] %john%');
 // But you will have raw Redis output.
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 4b547b7..90a313f 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -44,8 +44,8 @@
  * @method IndexBuilder withStructure(string $type)
  * @method IndexBuilder setPrefixes(array $prefixes)
  * @method IndexBuilder withPrefixes(array $prefixes)
- * @method IndexBuilder addPrefix(string ...$prefixes)
- * @method IndexBuilder withAddedPrefix(string ...$prefixes)
+ * @method IndexBuilder addPrefixes(string ...$prefixes)
+ * @method IndexBuilder withAddedPrefixes(string ...$prefixes)
  * @method IndexBuilder setFilter(string $filter)
  * @method IndexBuilder withFilter(string $filter)
  * @method IndexBuilder setDefaultLanguage(string $language)
@@ -74,11 +74,12 @@
  * @method IndexBuilder withSkipInitialScan(bool $active)
  * @method IndexBuilder setStopWords(array $words)
  * @method IndexBuilder withStopWords(array $words)
- * @method IndexBuilder addStopWord(string ...$words)
- * @method IndexBuilder withAddedStopWord(string ...$words)
+ * @method IndexBuilder addStopWords(string ...$words)
+ * @method IndexBuilder withAddedStopWords(string ...$words)
  * @method IndexBuilder setFields(array $fields)
  * @method IndexBuilder withFields(array $fields)
- * @method IndexBuilder withAddedField(CreateCommandFieldOption ...$fields)
+ * @method IndexBuilder addFields(CreateCommandFieldOption ...$fields)
+ * @method IndexBuilder withAddedFields(CreateCommandFieldOption ...$fields)
  * @method IndexBuilder withAddedTextField(string $name, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false)
  * @method IndexBuilder withAddedNumericField(string $name, bool $sortable = false, bool $noIndex = false)
  * @method IndexBuilder withAddedGeoField(string $name, bool $noIndex = false)
@@ -153,16 +154,9 @@ public function setNoStopWords(): self
         return $this;
     }
 
-    public function withNoStopWords(): self
+    public function addField(CreateCommandFieldOption $option): self
     {
-        $newInstance = clone $this;
-
-        return $newInstance->setNoStopWords();
-    }
-
-    public function addField(CreateCommandFieldOption ...$option): self
-    {
-        array_push($this->fields, ...$option);
+        $this->fields[] = $option;
 
         return $this;
     }
@@ -278,7 +272,7 @@ private function withAddedCall(string $name, array $arguments): self
     {
         $newInstance = clone $this;
 
-        return $newInstance->{'add'.$name}($arguments);
+        return $newInstance->{'add'.$name}(...$arguments);
     }
 
     /**
@@ -319,6 +313,9 @@ private function addCall(string $name, array $arguments): self
             throw new InvalidArgumentException(sprintf('add%s method need at least one parameter', $name));
         }
 
+        if (null === $this->{$propertyName}) {
+            $this->{$propertyName} = [];
+        }
         array_push($this->{$propertyName}, ...$arguments);
 
         return $this;
diff --git a/src/Redis/Client/Rediska/RediskaRediSearchCommand.php b/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
new file mode 100644
index 0000000..bda7822
--- /dev/null
+++ b/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Client\Rediska;
+
+use function count;
+use MacFJA\RediSearch\Redis\Client\AbstractClient;
+use Rediska_Command_Abstract;
+use Rediska_Connection_Exec;
+
+class RediskaRediSearchCommand extends Rediska_Command_Abstract
+{
+    public function create(): Rediska_Connection_Exec
+    {
+        $connections = $this->_rediska->getConnections();
+
+        if (false === AbstractClient::$disableNotice && count($connections) > 1) {
+            trigger_error('Warning, Multiple redis connections exists, only the first connection will be used', E_USER_NOTICE);
+        }
+        $connection = reset($connections);
+
+        $commands = $this->_arguments;
+        if (!('__redisearch' === $this->_name)) {
+            array_unshift($commands, $this->_name);
+        }
+
+        return new Rediska_Connection_Exec($connection, $commands);
+    }
+}
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index e5bb67b..6f988c8 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -21,8 +21,7 @@
 
 namespace MacFJA\RediSearch\Redis;
 
-use function count;
-use MacFJA\RediSearch\Redis\Client\AbstractClient;
+use MacFJA\RediSearch\Redis\Client\Rediska\RediskaRediSearchCommand;
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
 use MacFJA\RediSearch\Redis\Command\AliasDel;
@@ -51,34 +50,7 @@
 use MacFJA\RediSearch\Redis\Command\SynUpdate;
 use MacFJA\RediSearch\Redis\Command\TagVals;
 use Predis\Profile\RedisProfile;
-use Rediska_Command_Abstract;
 use Rediska_Commands;
-use Rediska_Connection_Exec;
-
-if (class_exists(Rediska_Command_Abstract::class) && class_exists(Rediska_Connection_Exec::class)) {
-    /**
-     * @codeCoverageIgnore
-     */
-    class RediskaRediSearch extends Rediska_Command_Abstract
-    {
-        public function create(): Rediska_Connection_Exec
-        {
-            $connections = $this->_rediska->getConnections();
-
-            if (false === AbstractClient::$disableNotice && count($connections) > 1) {
-                trigger_error('Warning, Multiple redis connections exists, only the first connection will be used', E_USER_NOTICE);
-            }
-            $connection = reset($connections);
-
-            $commands = $this->_arguments;
-            if (!('__redisearch' === $this->_name)) {
-                array_unshift($commands, $this->_name);
-            }
-
-            return new Rediska_Connection_Exec($connection, $commands);
-        }
-    }
-}
 
 /**
  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -124,34 +96,34 @@ public static function registerCommandsPredis(RedisProfile $profile): void
      */
     public static function registerCommandsRediska(): void
     {
-        Rediska_Commands::add('ftaggregate', RediskaRediSearch::class);
-        Rediska_Commands::add('ftaliasadd', RediskaRediSearch::class);
-        Rediska_Commands::add('ftaliasdel', RediskaRediSearch::class);
-        Rediska_Commands::add('ftaliasupdate', RediskaRediSearch::class);
-        Rediska_Commands::add('ftalter', RediskaRediSearch::class);
-        Rediska_Commands::add('ftconfig', RediskaRediSearch::class);
-        Rediska_Commands::add('ftconfigset', RediskaRediSearch::class);
-        Rediska_Commands::add('ftcreate', RediskaRediSearch::class);
-        Rediska_Commands::add('ftcursordel', RediskaRediSearch::class);
-        Rediska_Commands::add('ftcursorread', RediskaRediSearch::class);
-        Rediska_Commands::add('ftdictadd', RediskaRediSearch::class);
-        Rediska_Commands::add('ftdictdel', RediskaRediSearch::class);
-        Rediska_Commands::add('ftdictdump', RediskaRediSearch::class);
-        Rediska_Commands::add('ftdropindex', RediskaRediSearch::class);
-        Rediska_Commands::add('ftexplain', RediskaRediSearch::class);
-        Rediska_Commands::add('ftexplaincli', RediskaRediSearch::class);
-        Rediska_Commands::add('ftlist', RediskaRediSearch::class);
-        Rediska_Commands::add('ftinfo', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsearch', RediskaRediSearch::class);
-        Rediska_Commands::add('ftspellcheck', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsugadd', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsugdel', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsugget', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsuglen', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsyndump', RediskaRediSearch::class);
-        Rediska_Commands::add('ftsynupdate', RediskaRediSearch::class);
-        Rediska_Commands::add('fttagvals', RediskaRediSearch::class);
-        Rediska_Commands::add('__redisearch', RediskaRediSearch::class);
+        Rediska_Commands::add('ftaggregate', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftaliasadd', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftaliasdel', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftaliasupdate', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftalter', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftconfig', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftconfigset', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftcreate', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftcursordel', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftcursorread', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftdictadd', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftdictdel', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftdictdump', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftdropindex', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftexplain', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftexplaincli', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftlist', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftinfo', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsearch', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftspellcheck', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsugadd', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsugdel', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsugget', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsuglen', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsyndump', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('ftsynupdate', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('fttagvals', RediskaRediSearchCommand::class);
+        Rediska_Commands::add('__redisearch', RediskaRediSearchCommand::class);
     }
 
     public static function getRediSearchVersion(Client $client): ?string
diff --git a/tests/IndexBuilderTest.php b/tests/IndexBuilderTest.php
new file mode 100644
index 0000000..6587e5c
--- /dev/null
+++ b/tests/IndexBuilderTest.php
@@ -0,0 +1,358 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests;
+
+use BadMethodCallException;
+use InvalidArgumentException;
+use MacFJA\RediSearch\IndexBuilder;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command\Create;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use PHPUnit\Framework\TestCase;
+use RuntimeException;
+
+/**
+ * @covers \MacFJA\RediSearch\IndexBuilder
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Create
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\BaseCreateFieldOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait
+ *
+ * @group default
+ *
+ * @internal
+ */
+class IndexBuilderTest extends TestCase
+{
+    /**
+     * @return array<Create|IndexBuilder>
+     * @medium
+     */
+    public function testAllSetter(): array
+    {
+        $createCommand = new Create();
+
+        $builder = new IndexBuilder();
+
+        $builder->setIndex('city');
+        static::assertNotEquals($createCommand, $builder->getCommand());
+        $createCommand->setIndex('city');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $resultInstance = $builder->setPrefixes(['city-', 'c-']);
+        $createCommand->setPrefixes('city-', 'c-');
+        static::assertSame($builder, $resultInstance);
+
+        $builder->setStopWords(['hello', 'world']);
+        $createCommand->setStopWords('hello', 'world');
+
+        $builder->setSkipInitialScan(true);
+        $createCommand->setSkipInitialScan(true);
+
+        $builder->setNoFrequencies(true);
+        $createCommand->setNoFrequencies(true);
+
+        $builder->setNoFields(true);
+        $createCommand->setNoFields(true);
+
+        $builder->setNoHighLight(true);
+        $createCommand->setNoHighLight(true);
+
+        $builder->setTemporary(20);
+        $createCommand->setTemporary(20);
+
+        $builder->setNoOffsets(true);
+        $createCommand->setNoOffsets(true);
+
+        $builder->setDefaultLanguage('en');
+        $createCommand->setDefaultLanguage('en');
+
+        $builder->setDefaultScore(1.3);
+        $createCommand->setDefaultScore(1.3);
+
+        $builder->setMaxTextFields(true);
+        $createCommand->setMaxTextFields(true);
+
+        $builder->setPayloadField('payload');
+        $createCommand->setPayloadField('payload');
+
+        $builder->setScoreField('score');
+        $createCommand->setScoreField('score');
+
+        $builder->setLanguageField('lang');
+        $createCommand->setLanguageField('lang');
+
+        $builder->setFilter('startswith(@name, "G")');
+        $createCommand->setFilter('startswith(@name, "G")');
+
+        $builder->setStructure('JSON');
+        $createCommand->setStructure('JSON');
+
+        $builder->setFields([(new TextFieldOption())->setField('name')]);
+        $createCommand->addTextField('name');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        return [$builder, $createCommand];
+    }
+
+    /**
+     * @medium
+     */
+    public function testAllWith(): void
+    {
+        $createCommand = new Create();
+
+        $builder = new IndexBuilder();
+
+        $builder->setIndex('city');
+        $createCommand->setIndex('city');
+
+        $oldBuilder = $builder;
+        $builder = $builder->withPrefixes(['city-', 'c-']);
+        static::assertNotEquals($createCommand, $builder->getCommand());
+        $createCommand->setPrefixes('city-', 'c-');
+        static::assertEquals($createCommand, $builder->getCommand());
+        static::assertNotSame($oldBuilder, $builder);
+
+        $builder = $builder->withStopWords(['hello', 'world']);
+        $createCommand->setStopWords('hello', 'world');
+
+        $builder = $builder->withSkipInitialScan(true);
+        $createCommand->setSkipInitialScan(true);
+
+        $builder = $builder->withNoFrequencies(true);
+        $createCommand->setNoFrequencies(true);
+
+        $builder = $builder->withNoFields(true);
+        $createCommand->setNoFields(true);
+
+        $builder = $builder->withNoHighLight(true);
+        $createCommand->setNoHighLight(true);
+
+        $builder = $builder->withTemporary(20);
+        $createCommand->setTemporary(20);
+
+        $builder = $builder->withNoOffsets(true);
+        $createCommand->setNoOffsets(true);
+
+        $builder = $builder->withDefaultLanguage('en');
+        $createCommand->setDefaultLanguage('en');
+
+        $builder = $builder->withDefaultScore(1.3);
+        $createCommand->setDefaultScore(1.3);
+
+        $builder = $builder->withMaxTextFields(true);
+        $createCommand->setMaxTextFields(true);
+
+        $builder = $builder->withPayloadField('payload');
+        $createCommand->setPayloadField('payload');
+
+        $builder = $builder->withScoreField('score');
+        $createCommand->setScoreField('score');
+
+        $builder = $builder->withLanguageField('lang');
+        $createCommand->setLanguageField('lang');
+
+        $builder = $builder->withFilter('startswith(@name, "G")');
+        $createCommand->setFilter('startswith(@name, "G")');
+
+        $builder = $builder->withStructure('JSON');
+        $createCommand->setStructure('JSON');
+
+        $builder = $builder->withFields([(new TextFieldOption())->setField('name')]);
+        $createCommand->addTextField('name');
+        static::assertEquals($createCommand, $builder->getCommand());
+    }
+
+    public function testNoStopWords(): void
+    {
+        $createCommand = new Create();
+
+        $builder = new IndexBuilder();
+
+        $builder->setIndex('city');
+        $createCommand->setIndex('city');
+
+        $builder->setNoStopWords();
+        $createCommand->setNoStopWords();
+
+        static::assertEquals($createCommand, $builder->getCommand());
+    }
+
+    public function testAllAdd(): void
+    {
+        $createCommand = new Create();
+
+        $builder = new IndexBuilder();
+
+        $builder->setIndex('city');
+        $createCommand->setIndex('city');
+
+        $oldBuilder = $builder;
+        $builder = $builder->addPrefixes('city-', 'c-');
+        static::assertNotEquals($createCommand, $builder->getCommand());
+        $createCommand->setPrefixes('city-', 'c-');
+        static::assertEquals($createCommand, $builder->getCommand());
+        static::assertSame($oldBuilder, $builder);
+
+        $builder->addStopWords('hello', 'world');
+        $createCommand->setStopWords('hello', 'world');
+
+        $builder->addFields((new TextFieldOption())->setField('name'));
+        $createCommand->addTextField('name');
+
+        $builder->addField((new TextFieldOption())->setField('country'));
+        $createCommand->addTextField('country');
+
+        $builder->addTextField('continent');
+        $createCommand->addTextField('continent');
+
+        $builder->addNumericField('population');
+        $createCommand->addNumericField('population');
+
+        $builder->addGeoField('gps');
+        $createCommand->addGeoField('gps');
+
+        $builder->addTagField('languages');
+        $createCommand->addTagField('languages');
+        static::assertEquals($createCommand, $builder->getCommand());
+    }
+
+    /**
+     * @medium
+     */
+    public function testAllWithAdded(): void
+    {
+        $createCommand = new Create();
+
+        $builder = new IndexBuilder();
+
+        $builder->setIndex('city');
+        $createCommand->setIndex('city');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $oldBuilder = $builder;
+        $builder = $builder->withAddedPrefixes('city-', 'c-');
+        $createCommand->setPrefixes('city-', 'c-');
+        static::assertEquals($createCommand, $builder->getCommand());
+        static::assertNotSame($oldBuilder, $builder);
+
+        $builder = $builder->withAddedStopWords('hello', 'world');
+        $createCommand->setStopWords('hello', 'world');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $builder = $builder->withAddedFields((new TextFieldOption())->setField('name'));
+        $createCommand->addTextField('name');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $builder = $builder->withAddedTextField('continent');
+        $createCommand->addTextField('continent');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $builder = $builder->withAddedNumericField('population');
+        $createCommand->addNumericField('population');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $builder = $builder->withAddedGeoField('gps');
+        $createCommand->addGeoField('gps');
+        static::assertEquals($createCommand, $builder->getCommand());
+
+        $builder = $builder->withAddedTagField('languages');
+        $createCommand->addTagField('languages');
+        static::assertEquals($createCommand, $builder->getCommand());
+    }
+
+    public function testUnexistingMethod(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('Call undefined foobar method');
+
+        $builder = new IndexBuilder();
+        // @phpstan-ignore-next-line
+        $builder->foobar();
+    }
+
+    public function testUnexistingMethod2(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('Call undefined setHelloWorld method');
+
+        $builder = new IndexBuilder();
+        // @phpstan-ignore-next-line
+        $builder->setHelloWorld();
+    }
+
+    public function testUnexistingMethodAdd(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('Call undefined addHelloWorld method');
+
+        $builder = new IndexBuilder();
+        // @phpstan-ignore-next-line
+        $builder->addHelloWorld();
+    }
+
+    public function testInvalidAdd(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('addFields method need at least one parameter');
+
+        $builder = new IndexBuilder();
+        $builder->addFields();
+    }
+
+    public function testMissingIndex(): void
+    {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessage('The index name is missing');
+
+        $builder = new IndexBuilder();
+        $builder->getCommand();
+    }
+
+    public function testCreate(): void
+    {
+        /**
+         * @var IndexBuilder $builder
+         * @var Create       $createCommand
+         */
+        [$builder, $createCommand] = $this->testAllSetter();
+        $client = $this->createMock(Client::class);
+        $client->expects(static::once())->method('execute')->with($createCommand);
+        $builder->create($client);
+    }
+}
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 1333b71..22ec2a2 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -57,6 +57,7 @@
  * @covers \MacFJA\RediSearch\Redis\Client\CredisClient
  * @covers \MacFJA\RediSearch\Redis\Client\PredisClient
  * @covers \MacFJA\RediSearch\Redis\Client\RedisentClient
+ * @covers \MacFJA\RediSearch\Redis\Client\Rediska\RediskaRediSearchCommand
  * @covers \MacFJA\RediSearch\Redis\Client\RediskaClient
  * @covers \MacFJA\RediSearch\Redis\Client\TinyRedisClient
  * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
@@ -67,7 +68,6 @@
  * @covers \MacFJA\RediSearch\Redis\Command\SynDump
  * @covers \MacFJA\RediSearch\Redis\Command\SynUpdate
  * @covers \MacFJA\RediSearch\Redis\Initializer
- * @covers \MacFJA\RediSearch\Redis\RediskaRediSearch
  * @covers \MacFJA\RediSearch\Redis\Response\InfoResponse
  * @covers \MacFJA\RediSearch\Redis\Response\PaginatedResponse
  *

From 6d3a675c54fc5c9f675c50d860f2b3d50eab1972 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 6 Nov 2021 21:54:02 +0100
Subject: [PATCH 21/60] Version 2.0.0

---
 CHANGELOG.md  |    7 +-
 composer.lock | 7523 -------------------------------------------------
 2 files changed, 5 insertions(+), 7525 deletions(-)
 delete mode 100644 composer.lock

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a6e044..a3fc265 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.0.0]
+
 ### Added
 
 - Allow multiple connector for Redis ([PR#17])
@@ -121,8 +123,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/main...HEAD
-[Unreleased v1.x]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...HEAD
+[1.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/2.0.0
+[Unreleased v1.x]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...1.x
 [1.4.0]: https://github.com/MacFJA/php-redisearch/compare/1.3.0...1.4.0
 [1.3.0]: https://github.com/MacFJA/php-redisearch/compare/1.2.0...1.3.0
 [1.2.0]: https://github.com/MacFJA/php-redisearch/compare/1.1.0...1.2.0
diff --git a/composer.lock b/composer.lock
deleted file mode 100644
index 97bd7eb..0000000
--- a/composer.lock
+++ /dev/null
@@ -1,7523 +0,0 @@
-{
-    "_readme": [
-        "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
-        "This file is @generated automatically"
-    ],
-    "content-hash": "7bb89cff485836217af5df89eec3ad68",
-    "packages": [
-        {
-            "name": "composer/semver",
-            "version": "3.2.5",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/composer/semver.git",
-                "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9",
-                "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.3.2 || ^7.0 || ^8.0"
-            },
-            "require-dev": {
-                "phpstan/phpstan": "^0.12.54",
-                "symfony/phpunit-bridge": "^4.2 || ^5"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "3.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Composer\\Semver\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nils Adermann",
-                    "email": "naderman@naderman.de",
-                    "homepage": "http://www.naderman.de"
-                },
-                {
-                    "name": "Jordi Boggiano",
-                    "email": "j.boggiano@seld.be",
-                    "homepage": "http://seld.be"
-                },
-                {
-                    "name": "Rob Bast",
-                    "email": "rob.bast@gmail.com",
-                    "homepage": "http://robbast.nl"
-                }
-            ],
-            "description": "Semver library that offers utilities, version constraint parsing and validation.",
-            "keywords": [
-                "semantic",
-                "semver",
-                "validation",
-                "versioning"
-            ],
-            "support": {
-                "irc": "irc://irc.freenode.org/composer",
-                "issues": "https://github.com/composer/semver/issues",
-                "source": "https://github.com/composer/semver/tree/3.2.5"
-            },
-            "funding": [
-                {
-                    "url": "https://packagist.com",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/composer",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-05-24T12:41:47+00:00"
-        },
-        {
-            "name": "respect/stringifier",
-            "version": "0.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Respect/Stringifier.git",
-                "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
-                "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.8",
-                "malukenho/docheader": "^0.1.7",
-                "phpunit/phpunit": "^6.4"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Respect\\Stringifier\\": "src/"
-                },
-                "files": [
-                    "src/stringify.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Respect/Stringifier Contributors",
-                    "homepage": "https://github.com/Respect/Stringifier/graphs/contributors"
-                }
-            ],
-            "description": "Converts any value to a string",
-            "homepage": "http://respect.github.io/Stringifier/",
-            "keywords": [
-                "respect",
-                "stringifier",
-                "stringify"
-            ],
-            "support": {
-                "issues": "https://github.com/Respect/Stringifier/issues",
-                "source": "https://github.com/Respect/Stringifier/tree/0.2.0"
-            },
-            "time": "2017-12-29T19:39:25+00:00"
-        },
-        {
-            "name": "respect/validation",
-            "version": "2.0.17",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Respect/Validation.git",
-                "reference": "314aa36e37dd3812b7d80c7bda3e83da5ee22d11"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Respect/Validation/zipball/314aa36e37dd3812b7d80c7bda3e83da5ee22d11",
-                "reference": "314aa36e37dd3812b7d80c7bda3e83da5ee22d11",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.2",
-                "respect/stringifier": "^0.2.0",
-                "symfony/polyfill-mbstring": "^1.2"
-            },
-            "require-dev": {
-                "egulias/email-validator": "^2.1",
-                "malukenho/docheader": "^0.1",
-                "mikey179/vfsstream": "^1.6",
-                "phpstan/phpstan": "^0.12",
-                "phpstan/phpstan-deprecation-rules": "^0.12",
-                "phpstan/phpstan-phpunit": "^0.12",
-                "phpunit/phpunit": "^7.5",
-                "respect/coding-standard": "^2.1",
-                "squizlabs/php_codesniffer": "^3.5",
-                "symfony/validator": "^3.0||^4.0",
-                "zendframework/zend-validator": "^2.1"
-            },
-            "suggest": {
-                "egulias/email-validator": "Strict (RFC compliant) email validation",
-                "ext-bcmath": "Arbitrary Precision Mathematics",
-                "ext-fileinfo": "File Information",
-                "ext-mbstring": "Multibyte String Functions",
-                "symfony/validator": "Use Symfony validator through Respect\\Validation",
-                "zendframework/zend-validator": "Use Zend Framework validator through Respect\\Validation"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Respect\\Validation\\": "library/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Respect/Validation Contributors",
-                    "homepage": "https://github.com/Respect/Validation/graphs/contributors"
-                }
-            ],
-            "description": "The most awesome validation engine ever created for PHP",
-            "homepage": "http://respect.github.io/Validation/",
-            "keywords": [
-                "respect",
-                "validation",
-                "validator"
-            ],
-            "support": {
-                "issues": "https://github.com/Respect/Validation/issues",
-                "source": "https://github.com/Respect/Validation/tree/workflows/update-tld"
-            },
-            "time": "2020-08-23T00:24:43+00:00"
-        },
-        {
-            "name": "symfony/polyfill-mbstring",
-            "version": "v1.23.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
-                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "suggest": {
-                "ext-mbstring": "For best performance"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Mbstring\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill for the Mbstring extension",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "mbstring",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-05-27T12:26:48+00:00"
-        }
-    ],
-    "packages-dev": [
-        {
-            "name": "amphp/amp",
-            "version": "v2.6.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/amp.git",
-                "reference": "c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/amp/zipball/c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae",
-                "reference": "c5fc66a78ee38d7ac9195a37bacaf940eb3f65ae",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1",
-                "ext-json": "*",
-                "jetbrains/phpstorm-stubs": "^2019.3",
-                "phpunit/phpunit": "^7 | ^8 | ^9",
-                "psalm/phar": "^3.11@dev",
-                "react/promise": "^2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Amp\\": "lib"
-                },
-                "files": [
-                    "lib/functions.php",
-                    "lib/Internal/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Daniel Lowrey",
-                    "email": "rdlowrey@php.net"
-                },
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                },
-                {
-                    "name": "Bob Weinand",
-                    "email": "bobwei9@hotmail.com"
-                },
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "A non-blocking concurrency framework for PHP applications.",
-            "homepage": "http://amphp.org/amp",
-            "keywords": [
-                "async",
-                "asynchronous",
-                "awaitable",
-                "concurrency",
-                "event",
-                "event-loop",
-                "future",
-                "non-blocking",
-                "promise"
-            ],
-            "support": {
-                "irc": "irc://irc.freenode.org/amphp",
-                "issues": "https://github.com/amphp/amp/issues",
-                "source": "https://github.com/amphp/amp/tree/v2.6.1"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-09-23T18:43:08+00:00"
-        },
-        {
-            "name": "amphp/byte-stream",
-            "version": "v1.8.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/byte-stream.git",
-                "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
-                "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2",
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1.4",
-                "friendsofphp/php-cs-fixer": "^2.3",
-                "jetbrains/phpstorm-stubs": "^2019.3",
-                "phpunit/phpunit": "^6 || ^7 || ^8",
-                "psalm/phar": "^3.11.4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Amp\\ByteStream\\": "lib"
-                },
-                "files": [
-                    "lib/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                },
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "A stream abstraction to make working with non-blocking I/O simple.",
-            "homepage": "http://amphp.org/byte-stream",
-            "keywords": [
-                "amp",
-                "amphp",
-                "async",
-                "io",
-                "non-blocking",
-                "stream"
-            ],
-            "support": {
-                "irc": "irc://irc.freenode.org/amphp",
-                "issues": "https://github.com/amphp/byte-stream/issues",
-                "source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-03-30T17:13:30+00:00"
-        },
-        {
-            "name": "amphp/cache",
-            "version": "v1.5.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/cache.git",
-                "reference": "2b6b5dbb70e54cc914df9952ba7c012bc4cbcd28"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/cache/zipball/2b6b5dbb70e54cc914df9952ba7c012bc4cbcd28",
-                "reference": "2b6b5dbb70e54cc914df9952ba7c012bc4cbcd28",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2",
-                "amphp/serialization": "^1",
-                "amphp/sync": "^1.2",
-                "php": ">=7.1"
-            },
-            "conflict": {
-                "amphp/file": "<0.2 || >=3"
-            },
-            "require-dev": {
-                "amphp/file": "^1 || ^2",
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1.1",
-                "phpunit/phpunit": "^6 | ^7 | ^8 | ^9",
-                "vimeo/psalm": "^4"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Cache\\": "lib"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                },
-                {
-                    "name": "Daniel Lowrey",
-                    "email": "rdlowrey@php.net"
-                }
-            ],
-            "description": "A promise-aware caching API for Amp.",
-            "homepage": "https://github.com/amphp/cache",
-            "support": {
-                "irc": "irc://irc.freenode.org/amphp",
-                "issues": "https://github.com/amphp/cache/issues",
-                "source": "https://github.com/amphp/cache/tree/v1.5.0"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-06-29T17:12:43+00:00"
-        },
-        {
-            "name": "amphp/dns",
-            "version": "v1.2.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/dns.git",
-                "reference": "852292532294d7972c729a96b49756d781f7c59d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/dns/zipball/852292532294d7972c729a96b49756d781f7c59d",
-                "reference": "852292532294d7972c729a96b49756d781f7c59d",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2",
-                "amphp/byte-stream": "^1.1",
-                "amphp/cache": "^1.2",
-                "amphp/parser": "^1",
-                "amphp/windows-registry": "^0.3",
-                "daverandom/libdns": "^2.0.1",
-                "ext-filter": "*",
-                "ext-json": "*",
-                "php": ">=7.0"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1",
-                "phpunit/phpunit": "^6 || ^7 || ^8 || ^9"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Dns\\": "lib"
-                },
-                "files": [
-                    "lib/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Chris Wright",
-                    "email": "addr@daverandom.com"
-                },
-                {
-                    "name": "Daniel Lowrey",
-                    "email": "rdlowrey@php.net"
-                },
-                {
-                    "name": "Bob Weinand",
-                    "email": "bobwei9@hotmail.com"
-                },
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                },
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                }
-            ],
-            "description": "Async DNS resolution for Amp.",
-            "homepage": "https://github.com/amphp/dns",
-            "keywords": [
-                "amp",
-                "amphp",
-                "async",
-                "client",
-                "dns",
-                "resolve"
-            ],
-            "support": {
-                "issues": "https://github.com/amphp/dns/issues",
-                "source": "https://github.com/amphp/dns/tree/v1.2.3"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-07-21T19:04:57+00:00"
-        },
-        {
-            "name": "amphp/parser",
-            "version": "v1.0.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/parser.git",
-                "reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/parser/zipball/f83e68f03d5b8e8e0365b8792985a7f341c57ae1",
-                "reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7"
-            },
-            "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.3",
-                "phpunit/phpunit": "^6"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Parser\\": "lib"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                },
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                }
-            ],
-            "description": "A generator parser to make streaming parsers simple.",
-            "homepage": "https://github.com/amphp/parser",
-            "keywords": [
-                "async",
-                "non-blocking",
-                "parser",
-                "stream"
-            ],
-            "support": {
-                "issues": "https://github.com/amphp/parser/issues",
-                "source": "https://github.com/amphp/parser/tree/is-valid"
-            },
-            "time": "2017-06-06T05:29:10+00:00"
-        },
-        {
-            "name": "amphp/process",
-            "version": "v1.1.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/process.git",
-                "reference": "3d36327bf9b4c158cf3010f8f65c00854ec3a8b7"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/process/zipball/3d36327bf9b4c158cf3010f8f65c00854ec3a8b7",
-                "reference": "3d36327bf9b4c158cf3010f8f65c00854ec3a8b7",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2",
-                "amphp/byte-stream": "^1.4",
-                "php": ">=7"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1",
-                "phpunit/phpunit": "^6"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Process\\": "lib"
-                },
-                "files": [
-                    "lib/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Bob Weinand",
-                    "email": "bobwei9@hotmail.com"
-                },
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                },
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "Asynchronous process manager.",
-            "homepage": "https://github.com/amphp/process",
-            "support": {
-                "issues": "https://github.com/amphp/process/issues",
-                "source": "https://github.com/amphp/process/tree/v1.1.2"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-10-08T15:55:53+00:00"
-        },
-        {
-            "name": "amphp/redis",
-            "version": "v1.0.5",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/redis.git",
-                "reference": "b8910b1051b128105f0724b5c95c34db30bd1d45"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/redis/zipball/b8910b1051b128105f0724b5c95c34db30bd1d45",
-                "reference": "b8910b1051b128105f0724b5c95c34db30bd1d45",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2.2.1",
-                "amphp/cache": "^1.2.1",
-                "amphp/socket": "^1",
-                "amphp/sync": "^1.1",
-                "league/uri-parser": "^1.4",
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1.1.2",
-                "phpunit/phpunit": "^7 | ^8"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Redis\\": "src"
-                },
-                "files": [
-                    "src/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "A non-blocking Redis client for Amp.",
-            "homepage": "https://github.com/amphp/redis",
-            "keywords": [
-                "amp",
-                "amphp",
-                "async",
-                "client",
-                "redis"
-            ],
-            "support": {
-                "issues": "https://github.com/amphp/redis/issues",
-                "source": "https://github.com/amphp/redis/tree/v1.0.5"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-10-12T18:38:52+00:00"
-        },
-        {
-            "name": "amphp/serialization",
-            "version": "v1.0.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/serialization.git",
-                "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1",
-                "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "phpunit/phpunit": "^9 || ^8 || ^7"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Serialization\\": "src"
-                },
-                "files": [
-                    "src/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                },
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "Serialization tools for IPC and data storage in PHP.",
-            "homepage": "https://github.com/amphp/serialization",
-            "keywords": [
-                "async",
-                "asynchronous",
-                "serialization",
-                "serialize"
-            ],
-            "support": {
-                "issues": "https://github.com/amphp/serialization/issues",
-                "source": "https://github.com/amphp/serialization/tree/master"
-            },
-            "time": "2020-03-25T21:39:07+00:00"
-        },
-        {
-            "name": "amphp/socket",
-            "version": "v1.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/socket.git",
-                "reference": "a8af9f5d0a66c5fe9567da45a51509e592788fe6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/socket/zipball/a8af9f5d0a66c5fe9567da45a51509e592788fe6",
-                "reference": "a8af9f5d0a66c5fe9567da45a51509e592788fe6",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2",
-                "amphp/byte-stream": "^1.6",
-                "amphp/dns": "^1 || ^0.9",
-                "ext-openssl": "*",
-                "kelunik/certificate": "^1.1",
-                "league/uri-parser": "^1.4",
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1",
-                "phpunit/phpunit": "^6 || ^7 || ^8",
-                "vimeo/psalm": "^3.9@dev"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Socket\\": "src"
-                },
-                "files": [
-                    "src/functions.php",
-                    "src/Internal/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Daniel Lowrey",
-                    "email": "rdlowrey@gmail.com"
-                },
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                },
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "Async socket connection / server tools for Amp.",
-            "homepage": "https://github.com/amphp/socket",
-            "keywords": [
-                "amp",
-                "async",
-                "encryption",
-                "non-blocking",
-                "sockets",
-                "tcp",
-                "tls"
-            ],
-            "support": {
-                "issues": "https://github.com/amphp/socket/issues",
-                "source": "https://github.com/amphp/socket/tree/v1.2.0"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-07-09T18:18:48+00:00"
-        },
-        {
-            "name": "amphp/sync",
-            "version": "v1.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/sync.git",
-                "reference": "613047ac54c025aa800a9cde5b05c3add7327ed4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/sync/zipball/613047ac54c025aa800a9cde5b05c3add7327ed4",
-                "reference": "613047ac54c025aa800a9cde5b05c3add7327ed4",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2.2",
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master",
-                "amphp/phpunit-util": "^1.1",
-                "phpunit/phpunit": "^9 || ^8 || ^7"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\Sync\\": "src"
-                },
-                "files": [
-                    "src/functions.php",
-                    "src/ConcurrentIterator/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Aaron Piotrowski",
-                    "email": "aaron@trowski.com"
-                },
-                {
-                    "name": "Stephen Coakley",
-                    "email": "me@stephencoakley.com"
-                }
-            ],
-            "description": "Mutex, Semaphore, and other synchronization tools for Amp.",
-            "homepage": "https://github.com/amphp/sync",
-            "keywords": [
-                "async",
-                "asynchronous",
-                "mutex",
-                "semaphore",
-                "synchronization"
-            ],
-            "support": {
-                "issues": "https://github.com/amphp/sync/issues",
-                "source": "https://github.com/amphp/sync/tree/v1.4.0"
-            },
-            "time": "2020-05-07T18:57:50+00:00"
-        },
-        {
-            "name": "amphp/windows-registry",
-            "version": "v0.3.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/amphp/windows-registry.git",
-                "reference": "0f56438b9197e224325e88f305346f0221df1f71"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/amphp/windows-registry/zipball/0f56438b9197e224325e88f305346f0221df1f71",
-                "reference": "0f56438b9197e224325e88f305346f0221df1f71",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2",
-                "amphp/byte-stream": "^1.4",
-                "amphp/process": "^1"
-            },
-            "require-dev": {
-                "amphp/php-cs-fixer-config": "dev-master"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Amp\\WindowsRegistry\\": "lib"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "Windows Registry Reader.",
-            "support": {
-                "issues": "https://github.com/amphp/windows-registry/issues",
-                "source": "https://github.com/amphp/windows-registry/tree/master"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/amphp",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-07-10T16:13:29+00:00"
-        },
-        {
-            "name": "cheprasov/php-redis-client",
-            "version": "1.10.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/cheprasov/php-redis-client.git",
-                "reference": "c3594e0bbc8ee1bc2c11c889ce5dfa0d3b8a6f9a"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/cheprasov/php-redis-client/zipball/c3594e0bbc8ee1bc2c11c889ce5dfa0d3b8a6f9a",
-                "reference": "c3594e0bbc8ee1bc2c11c889ce5dfa0d3b8a6f9a",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.5"
-            },
-            "require-dev": {
-                "cheprasov/php-extra-mocks": "^1.0.0",
-                "phpunit/phpunit": "4.8.*"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "RedisClient\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Alexander Cheprasov",
-                    "email": "acheprasov84@gmail.com"
-                }
-            ],
-            "description": "Php client for Redis. It is a fast, fully-functional and user-friendly client for Redis, optimized for performance. RedisClient supports the latest versions of Redis starting from 2.6 to 6.0",
-            "homepage": "http://github.com/cheprasov/php-redis-client",
-            "support": {
-                "issues": "https://github.com/cheprasov/php-redis-client/issues",
-                "source": "https://github.com/cheprasov/php-redis-client/tree/v1.10.0"
-            },
-            "time": "2020-06-20T13:05:24+00:00"
-        },
-        {
-            "name": "colinmollenhour/credis",
-            "version": "v1.12.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/colinmollenhour/credis.git",
-                "reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/colinmollenhour/credis/zipball/c27faa11724229986335c23f4b6d0f1d8d6547fb",
-                "reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.4.0"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "Client.php",
-                    "Cluster.php",
-                    "Sentinel.php",
-                    "Module.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Colin Mollenhour",
-                    "email": "colin@mollenhour.com"
-                }
-            ],
-            "description": "Credis is a lightweight interface to the Redis key-value store which wraps the phpredis library when available for better performance.",
-            "homepage": "https://github.com/colinmollenhour/credis",
-            "support": {
-                "issues": "https://github.com/colinmollenhour/credis/issues",
-                "source": "https://github.com/colinmollenhour/credis/tree/v1.12.1"
-            },
-            "time": "2020-11-06T16:09:14+00:00"
-        },
-        {
-            "name": "composer/package-versions-deprecated",
-            "version": "1.11.99.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/composer/package-versions-deprecated.git",
-                "reference": "b174585d1fe49ceed21928a945138948cb394600"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600",
-                "reference": "b174585d1fe49ceed21928a945138948cb394600",
-                "shasum": ""
-            },
-            "require": {
-                "composer-plugin-api": "^1.1.0 || ^2.0",
-                "php": "^7 || ^8"
-            },
-            "replace": {
-                "ocramius/package-versions": "1.11.99"
-            },
-            "require-dev": {
-                "composer/composer": "^1.9.3 || ^2.0@dev",
-                "ext-zip": "^1.13",
-                "phpunit/phpunit": "^6.5 || ^7"
-            },
-            "type": "composer-plugin",
-            "extra": {
-                "class": "PackageVersions\\Installer",
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "PackageVersions\\": "src/PackageVersions"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Marco Pivetta",
-                    "email": "ocramius@gmail.com"
-                },
-                {
-                    "name": "Jordi Boggiano",
-                    "email": "j.boggiano@seld.be"
-                }
-            ],
-            "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
-            "support": {
-                "issues": "https://github.com/composer/package-versions-deprecated/issues",
-                "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4"
-            },
-            "funding": [
-                {
-                    "url": "https://packagist.com",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/composer",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-09-13T08:41:34+00:00"
-        },
-        {
-            "name": "composer/xdebug-handler",
-            "version": "2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/composer/xdebug-handler.git",
-                "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339",
-                "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.3.2 || ^7.0 || ^8.0",
-                "psr/log": "^1 || ^2 || ^3"
-            },
-            "require-dev": {
-                "phpstan/phpstan": "^0.12.55",
-                "symfony/phpunit-bridge": "^4.2 || ^5"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Composer\\XdebugHandler\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "John Stevenson",
-                    "email": "john-stevenson@blueyonder.co.uk"
-                }
-            ],
-            "description": "Restarts a process without Xdebug.",
-            "keywords": [
-                "Xdebug",
-                "performance"
-            ],
-            "support": {
-                "irc": "irc://irc.freenode.org/composer",
-                "issues": "https://github.com/composer/xdebug-handler/issues",
-                "source": "https://github.com/composer/xdebug-handler/tree/2.0.2"
-            },
-            "funding": [
-                {
-                    "url": "https://packagist.com",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/composer",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-31T17:03:58+00:00"
-        },
-        {
-            "name": "daverandom/libdns",
-            "version": "v2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/DaveRandom/LibDNS.git",
-                "reference": "e8b6d6593d18ac3a6a14666d8a68a4703b2e05f9"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/e8b6d6593d18ac3a6a14666d8a68a4703b2e05f9",
-                "reference": "e8b6d6593d18ac3a6a14666d8a68a4703b2e05f9",
-                "shasum": ""
-            },
-            "require": {
-                "ext-ctype": "*",
-                "php": ">=7.0"
-            },
-            "suggest": {
-                "ext-intl": "Required for IDN support"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "LibDNS\\": "src/"
-                },
-                "files": [
-                    "src/functions.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "description": "DNS protocol implementation written in pure PHP",
-            "keywords": [
-                "dns"
-            ],
-            "support": {
-                "issues": "https://github.com/DaveRandom/LibDNS/issues",
-                "source": "https://github.com/DaveRandom/LibDNS/tree/v2.0.2"
-            },
-            "time": "2019-12-03T09:12:46+00:00"
-        },
-        {
-            "name": "dnoegel/php-xdg-base-dir",
-            "version": "v0.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/dnoegel/php-xdg-base-dir.git",
-                "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
-                "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "XdgBaseDir\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "description": "implementation of xdg base directory specification for php",
-            "support": {
-                "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues",
-                "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1"
-            },
-            "time": "2019-12-04T15:06:13+00:00"
-        },
-        {
-            "name": "doctrine/annotations",
-            "version": "1.13.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/annotations.git",
-                "reference": "5b668aef16090008790395c02c893b1ba13f7e08"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08",
-                "reference": "5b668aef16090008790395c02c893b1ba13f7e08",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/lexer": "1.*",
-                "ext-tokenizer": "*",
-                "php": "^7.1 || ^8.0",
-                "psr/cache": "^1 || ^2 || ^3"
-            },
-            "require-dev": {
-                "doctrine/cache": "^1.11 || ^2.0",
-                "doctrine/coding-standard": "^6.0 || ^8.1",
-                "phpstan/phpstan": "^0.12.20",
-                "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
-                "symfony/cache": "^4.4 || ^5.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "guilhermeblanco@gmail.com"
-                },
-                {
-                    "name": "Roman Borschel",
-                    "email": "roman@code-factory.org"
-                },
-                {
-                    "name": "Benjamin Eberlei",
-                    "email": "kontakt@beberlei.de"
-                },
-                {
-                    "name": "Jonathan Wage",
-                    "email": "jonwage@gmail.com"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "schmittjoh@gmail.com"
-                }
-            ],
-            "description": "Docblock Annotations Parser",
-            "homepage": "https://www.doctrine-project.org/projects/annotations.html",
-            "keywords": [
-                "annotations",
-                "docblock",
-                "parser"
-            ],
-            "support": {
-                "issues": "https://github.com/doctrine/annotations/issues",
-                "source": "https://github.com/doctrine/annotations/tree/1.13.2"
-            },
-            "time": "2021-08-05T19:00:23+00:00"
-        },
-        {
-            "name": "doctrine/instantiator",
-            "version": "1.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
-                "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1 || ^8.0"
-            },
-            "require-dev": {
-                "doctrine/coding-standard": "^8.0",
-                "ext-pdo": "*",
-                "ext-phar": "*",
-                "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
-                "phpstan/phpstan": "^0.12",
-                "phpstan/phpstan-phpunit": "^0.12",
-                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Marco Pivetta",
-                    "email": "ocramius@gmail.com",
-                    "homepage": "https://ocramius.github.io/"
-                }
-            ],
-            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
-            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
-            "keywords": [
-                "constructor",
-                "instantiate"
-            ],
-            "support": {
-                "issues": "https://github.com/doctrine/instantiator/issues",
-                "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
-            },
-            "funding": [
-                {
-                    "url": "https://www.doctrine-project.org/sponsorship.html",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://www.patreon.com/phpdoctrine",
-                    "type": "patreon"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-11-10T18:47:58+00:00"
-        },
-        {
-            "name": "doctrine/lexer",
-            "version": "1.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/lexer.git",
-                "reference": "e864bbf5904cb8f5bb334f99209b48018522f042"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042",
-                "reference": "e864bbf5904cb8f5bb334f99209b48018522f042",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0"
-            },
-            "require-dev": {
-                "doctrine/coding-standard": "^6.0",
-                "phpstan/phpstan": "^0.11.8",
-                "phpunit/phpunit": "^8.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.2.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "guilhermeblanco@gmail.com"
-                },
-                {
-                    "name": "Roman Borschel",
-                    "email": "roman@code-factory.org"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "schmittjoh@gmail.com"
-                }
-            ],
-            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
-            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
-            "keywords": [
-                "annotations",
-                "docblock",
-                "lexer",
-                "parser",
-                "php"
-            ],
-            "support": {
-                "issues": "https://github.com/doctrine/lexer/issues",
-                "source": "https://github.com/doctrine/lexer/tree/1.2.1"
-            },
-            "funding": [
-                {
-                    "url": "https://www.doctrine-project.org/sponsorship.html",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://www.patreon.com/phpdoctrine",
-                    "type": "patreon"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-05-25T17:44:05+00:00"
-        },
-        {
-            "name": "enlightn/security-checker",
-            "version": "v1.9.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/enlightn/security-checker.git",
-                "reference": "dc5bce653fa4d9c792e9dcffa728c0642847c1e1"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/enlightn/security-checker/zipball/dc5bce653fa4d9c792e9dcffa728c0642847c1e1",
-                "reference": "dc5bce653fa4d9c792e9dcffa728c0642847c1e1",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "guzzlehttp/guzzle": "^6.3|^7.0",
-                "php": ">=5.6",
-                "symfony/console": "^3.4|^4|^5",
-                "symfony/finder": "^3|^4|^5",
-                "symfony/process": "^3.4|^4|^5",
-                "symfony/yaml": "^3.4|^4|^5"
-            },
-            "require-dev": {
-                "ext-zip": "*",
-                "friendsofphp/php-cs-fixer": "^2.18",
-                "phpunit/phpunit": "^5.5|^6|^7|^8|^9"
-            },
-            "bin": [
-                "security-checker"
-            ],
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Enlightn\\SecurityChecker\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Paras Malhotra",
-                    "email": "paras@laravel-enlightn.com"
-                },
-                {
-                    "name": "Miguel Piedrafita",
-                    "email": "soy@miguelpiedrafita.com"
-                }
-            ],
-            "description": "A PHP dependency vulnerabilities scanner based on the Security Advisories Database.",
-            "keywords": [
-                "package",
-                "php",
-                "scanner",
-                "security",
-                "security advisories",
-                "vulnerability scanner"
-            ],
-            "support": {
-                "issues": "https://github.com/enlightn/security-checker/issues",
-                "source": "https://github.com/enlightn/security-checker/tree/v1.9.0"
-            },
-            "time": "2021-05-06T09:03:35+00:00"
-        },
-        {
-            "name": "ergebnis/composer-normalize",
-            "version": "2.15.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ergebnis/composer-normalize.git",
-                "reference": "d469a15b916441959446d52a0f5d3fc9f7720317"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/d469a15b916441959446d52a0f5d3fc9f7720317",
-                "reference": "d469a15b916441959446d52a0f5d3fc9f7720317",
-                "shasum": ""
-            },
-            "require": {
-                "composer-plugin-api": "^1.1.0 || ^2.0.0",
-                "ergebnis/json-normalizer": "^1.0.3",
-                "ergebnis/json-printer": "^3.1.1",
-                "justinrainbow/json-schema": "^5.2.10",
-                "localheinz/diff": "^1.1.1",
-                "php": "^7.2 || ^8.0"
-            },
-            "require-dev": {
-                "composer/composer": "^1.10.22 || ^2.0.13",
-                "ergebnis/license": "^1.1.0",
-                "ergebnis/php-cs-fixer-config": "^2.14.0",
-                "ergebnis/phpstan-rules": "~0.15.3",
-                "ergebnis/test-util": "^1.5.0",
-                "phpstan/extension-installer": "^1.1.0",
-                "phpstan/phpstan": "~0.12.89",
-                "phpstan/phpstan-deprecation-rules": "~0.12.6",
-                "phpstan/phpstan-phpunit": "~0.12.19",
-                "phpstan/phpstan-strict-rules": "~0.12.9",
-                "phpunit/phpunit": "^8.5.16",
-                "psalm/plugin-phpunit": "~0.16.0",
-                "symfony/filesystem": "^5.3.0",
-                "vimeo/psalm": "^4.7.3"
-            },
-            "type": "composer-plugin",
-            "extra": {
-                "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin",
-                "composer-normalize": {
-                    "indent-size": 2,
-                    "indent-style": "space"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Ergebnis\\Composer\\Normalize\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Andreas Möller",
-                    "email": "am@localheinz.com"
-                }
-            ],
-            "description": "Provides a composer plugin for normalizing composer.json.",
-            "homepage": "https://github.com/ergebnis/composer-normalize",
-            "keywords": [
-                "composer",
-                "normalize",
-                "normalizer",
-                "plugin"
-            ],
-            "support": {
-                "issues": "https://github.com/ergebnis/composer-normalize/issues",
-                "source": "https://github.com/ergebnis/composer-normalize"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/localheinz",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-06-15T08:06:45+00:00"
-        },
-        {
-            "name": "ergebnis/json-normalizer",
-            "version": "1.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ergebnis/json-normalizer.git",
-                "reference": "4a7f064ce34d5a2e382564565cdd433dbc5b9494"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/4a7f064ce34d5a2e382564565cdd433dbc5b9494",
-                "reference": "4a7f064ce34d5a2e382564565cdd433dbc5b9494",
-                "shasum": ""
-            },
-            "require": {
-                "ergebnis/json-printer": "^3.1.1",
-                "ext-json": "*",
-                "justinrainbow/json-schema": "^5.2.10",
-                "php": "^7.2 || ^8.0"
-            },
-            "require-dev": {
-                "ergebnis/license": "^1.1.0",
-                "ergebnis/php-cs-fixer-config": "^2.10.0",
-                "ergebnis/phpstan-rules": "~0.15.3",
-                "ergebnis/test-util": "^1.4.0",
-                "infection/infection": "~0.15.3",
-                "jangregor/phpstan-prophecy": "~0.8.1",
-                "phpstan/extension-installer": "^1.1.0",
-                "phpstan/phpstan": "~0.12.80",
-                "phpstan/phpstan-deprecation-rules": "~0.12.6",
-                "phpstan/phpstan-phpunit": "~0.12.17",
-                "phpstan/phpstan-strict-rules": "~0.12.9",
-                "phpunit/phpunit": "^8.5.14",
-                "psalm/plugin-phpunit": "~0.12.2",
-                "vimeo/psalm": "^3.18"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Ergebnis\\Json\\Normalizer\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Andreas Möller",
-                    "email": "am@localheinz.com"
-                }
-            ],
-            "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.",
-            "homepage": "https://github.com/ergebnis/json-normalizer",
-            "keywords": [
-                "json",
-                "normalizer"
-            ],
-            "support": {
-                "issues": "https://github.com/ergebnis/json-normalizer/issues",
-                "source": "https://github.com/ergebnis/json-normalizer"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/localheinz",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-03-06T13:33:57+00:00"
-        },
-        {
-            "name": "ergebnis/json-printer",
-            "version": "3.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ergebnis/json-printer.git",
-                "reference": "e4190dadd9937a77d8afcaf2b6c42a528ab367d6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/e4190dadd9937a77d8afcaf2b6c42a528ab367d6",
-                "reference": "e4190dadd9937a77d8afcaf2b6c42a528ab367d6",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "ext-mbstring": "*",
-                "php": "^7.2 || ^8.0"
-            },
-            "require-dev": {
-                "ergebnis/license": "^1.0.0",
-                "ergebnis/php-cs-fixer-config": "^2.2.1",
-                "ergebnis/phpstan-rules": "~0.15.2",
-                "ergebnis/test-util": "^1.1.0",
-                "infection/infection": "~0.15.3",
-                "phpstan/extension-installer": "^1.0.4",
-                "phpstan/phpstan": "~0.12.40",
-                "phpstan/phpstan-deprecation-rules": "~0.12.5",
-                "phpstan/phpstan-phpunit": "~0.12.16",
-                "phpstan/phpstan-strict-rules": "~0.12.4",
-                "phpunit/phpunit": "^8.5.8",
-                "psalm/plugin-phpunit": "~0.11.0",
-                "vimeo/psalm": "^3.14.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Ergebnis\\Json\\Printer\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Andreas Möller",
-                    "email": "am@localheinz.com"
-                }
-            ],
-            "description": "Provides a JSON printer, allowing for flexible indentation.",
-            "homepage": "https://github.com/ergebnis/json-printer",
-            "keywords": [
-                "formatter",
-                "json",
-                "printer"
-            ],
-            "support": {
-                "issues": "https://github.com/ergebnis/json-printer/issues",
-                "source": "https://github.com/ergebnis/json-printer"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/localheinz",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-08-30T12:17:03+00:00"
-        },
-        {
-            "name": "felixfbecker/advanced-json-rpc",
-            "version": "v3.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git",
-                "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447",
-                "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447",
-                "shasum": ""
-            },
-            "require": {
-                "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
-                "php": "^7.1 || ^8.0",
-                "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^7.0 || ^8.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "AdvancedJsonRpc\\": "lib/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "ISC"
-            ],
-            "authors": [
-                {
-                    "name": "Felix Becker",
-                    "email": "felix.b@outlook.com"
-                }
-            ],
-            "description": "A more advanced JSONRPC implementation",
-            "support": {
-                "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues",
-                "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1"
-            },
-            "time": "2021-06-11T22:34:44+00:00"
-        },
-        {
-            "name": "felixfbecker/language-server-protocol",
-            "version": "1.5.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/felixfbecker/php-language-server-protocol.git",
-                "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
-                "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpstan/phpstan": "*",
-                "squizlabs/php_codesniffer": "^3.1",
-                "vimeo/psalm": "^4.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "LanguageServerProtocol\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "ISC"
-            ],
-            "authors": [
-                {
-                    "name": "Felix Becker",
-                    "email": "felix.b@outlook.com"
-                }
-            ],
-            "description": "PHP classes for the Language Server Protocol",
-            "keywords": [
-                "language",
-                "microsoft",
-                "php",
-                "server"
-            ],
-            "support": {
-                "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
-                "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1"
-            },
-            "time": "2021-02-22T14:02:09+00:00"
-        },
-        {
-            "name": "friendsofphp/php-cs-fixer",
-            "version": "v3.1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
-                "reference": "cf4cedb9e8991c2daa94a756176d81bf487e4c4b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/cf4cedb9e8991c2daa94a756176d81bf487e4c4b",
-                "reference": "cf4cedb9e8991c2daa94a756176d81bf487e4c4b",
-                "shasum": ""
-            },
-            "require": {
-                "composer/semver": "^3.2",
-                "composer/xdebug-handler": "^2.0",
-                "doctrine/annotations": "^1.12",
-                "ext-json": "*",
-                "ext-tokenizer": "*",
-                "php": "^7.1.3 || ^8.0",
-                "php-cs-fixer/diff": "^2.0",
-                "symfony/console": "^4.4.20 || ^5.1.3",
-                "symfony/event-dispatcher": "^4.4.20 || ^5.0",
-                "symfony/filesystem": "^4.4.20 || ^5.0",
-                "symfony/finder": "^4.4.20 || ^5.0",
-                "symfony/options-resolver": "^4.4.20 || ^5.0",
-                "symfony/polyfill-php72": "^1.23",
-                "symfony/polyfill-php81": "^1.23",
-                "symfony/process": "^4.4.20 || ^5.0",
-                "symfony/stopwatch": "^4.4.20 || ^5.0"
-            },
-            "require-dev": {
-                "justinrainbow/json-schema": "^5.2",
-                "keradus/cli-executor": "^1.4",
-                "mikey179/vfsstream": "^1.6.8",
-                "php-coveralls/php-coveralls": "^2.4.3",
-                "php-cs-fixer/accessible-object": "^1.1",
-                "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2",
-                "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1",
-                "phpspec/prophecy": "^1.10.3",
-                "phpspec/prophecy-phpunit": "^1.1 || ^2.0",
-                "phpunit/phpunit": "^7.5.20 || ^8.5.14 || ^9.5",
-                "phpunitgoodpractices/polyfill": "^1.5",
-                "phpunitgoodpractices/traits": "^1.9.1",
-                "symfony/phpunit-bridge": "^5.2.4",
-                "symfony/yaml": "^4.4.20 || ^5.0"
-            },
-            "suggest": {
-                "ext-dom": "For handling output formats in XML",
-                "ext-mbstring": "For handling non-UTF8 characters.",
-                "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible."
-            },
-            "bin": [
-                "php-cs-fixer"
-            ],
-            "type": "application",
-            "autoload": {
-                "psr-4": {
-                    "PhpCsFixer\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Dariusz Rumiński",
-                    "email": "dariusz.ruminski@gmail.com"
-                }
-            ],
-            "description": "A tool to automatically fix PHP code style",
-            "support": {
-                "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
-                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.1.0"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/keradus",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-08-29T20:16:20+00:00"
-        },
-        {
-            "name": "geometria-lab/rediska",
-            "version": "v0.5.10",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Shumkov/Rediska.git",
-                "reference": "5b33ba2f1526f9ad30864b42617e34ac370aeae0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Shumkov/Rediska/zipball/5b33ba2f1526f9ad30864b42617e34ac370aeae0",
-                "reference": "5b33ba2f1526f9ad30864b42617e34ac370aeae0",
-                "shasum": ""
-            },
-            "require-dev": {
-                "phpunit/phpunit": ">=3.7.11",
-                "zendframework/zendframework1": ">=1.12.1"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "Rediska": [
-                        "library/",
-                        "tests/library/"
-                    ]
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "authors": [
-                {
-                    "name": "Maxim Ivanov",
-                    "email": "maximiv@gmail.com"
-                },
-                {
-                    "name": "Ryan Grenz",
-                    "email": "info@ryangrenz.com"
-                },
-                {
-                    "name": "Ivan Shumkov",
-                    "email": "ivan@shumkov.ru"
-                },
-                {
-                    "name": "Till Klampaeckel",
-                    "email": "till@php.net"
-                }
-            ],
-            "description": "Full-featured PHP client for key-value database Redis",
-            "keywords": [
-                "cache",
-                "redis",
-                "sessions",
-                "zend"
-            ],
-            "support": {
-                "issues": "https://github.com/Shumkov/Rediska/issues",
-                "source": "https://github.com/Shumkov/Rediska/tree/v0.5.10"
-            },
-            "time": "2014-08-12T15:02:35+00:00"
-        },
-        {
-            "name": "guzzlehttp/guzzle",
-            "version": "6.5.5",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
-                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "guzzlehttp/promises": "^1.0",
-                "guzzlehttp/psr7": "^1.6.1",
-                "php": ">=5.5",
-                "symfony/polyfill-intl-idn": "^1.17.0"
-            },
-            "require-dev": {
-                "ext-curl": "*",
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
-                "psr/log": "^1.1"
-            },
-            "suggest": {
-                "psr/log": "Required for using the Log middleware"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "6.5-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "GuzzleHttp\\": "src/"
-                },
-                "files": [
-                    "src/functions_include.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Michael Dowling",
-                    "email": "mtdowling@gmail.com",
-                    "homepage": "https://github.com/mtdowling"
-                }
-            ],
-            "description": "Guzzle is a PHP HTTP client library",
-            "homepage": "http://guzzlephp.org/",
-            "keywords": [
-                "client",
-                "curl",
-                "framework",
-                "http",
-                "http client",
-                "rest",
-                "web service"
-            ],
-            "support": {
-                "issues": "https://github.com/guzzle/guzzle/issues",
-                "source": "https://github.com/guzzle/guzzle/tree/6.5"
-            },
-            "time": "2020-06-16T21:01:06+00:00"
-        },
-        {
-            "name": "guzzlehttp/promises",
-            "version": "1.4.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/guzzle/promises.git",
-                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
-                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.5"
-            },
-            "require-dev": {
-                "symfony/phpunit-bridge": "^4.4 || ^5.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "GuzzleHttp\\Promise\\": "src/"
-                },
-                "files": [
-                    "src/functions_include.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Michael Dowling",
-                    "email": "mtdowling@gmail.com",
-                    "homepage": "https://github.com/mtdowling"
-                }
-            ],
-            "description": "Guzzle promises library",
-            "keywords": [
-                "promise"
-            ],
-            "support": {
-                "issues": "https://github.com/guzzle/promises/issues",
-                "source": "https://github.com/guzzle/promises/tree/1.4.1"
-            },
-            "time": "2021-03-07T09:25:29+00:00"
-        },
-        {
-            "name": "guzzlehttp/psr7",
-            "version": "1.8.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/guzzle/psr7.git",
-                "reference": "dc960a912984efb74d0a90222870c72c87f10c91"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
-                "reference": "dc960a912984efb74d0a90222870c72c87f10c91",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.4.0",
-                "psr/http-message": "~1.0",
-                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
-            },
-            "provide": {
-                "psr/http-message-implementation": "1.0"
-            },
-            "require-dev": {
-                "ext-zlib": "*",
-                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
-            },
-            "suggest": {
-                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.7-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "GuzzleHttp\\Psr7\\": "src/"
-                },
-                "files": [
-                    "src/functions_include.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Michael Dowling",
-                    "email": "mtdowling@gmail.com",
-                    "homepage": "https://github.com/mtdowling"
-                },
-                {
-                    "name": "Tobias Schultze",
-                    "homepage": "https://github.com/Tobion"
-                }
-            ],
-            "description": "PSR-7 message implementation that also provides common utility methods",
-            "keywords": [
-                "http",
-                "message",
-                "psr-7",
-                "request",
-                "response",
-                "stream",
-                "uri",
-                "url"
-            ],
-            "support": {
-                "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/1.8.2"
-            },
-            "time": "2021-04-26T09:17:50+00:00"
-        },
-        {
-            "name": "insolita/unused-scanner",
-            "version": "2.3.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Insolita/unused-scanner.git",
-                "reference": "c52f871b138f24a8d98e000ca9f02b51f81fa900"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Insolita/unused-scanner/zipball/c52f871b138f24a8d98e000ca9f02b51f81fa900",
-                "reference": "c52f871b138f24a8d98e000ca9f02b51f81fa900",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "ext-mbstring": "*",
-                "php": ">=7.1",
-                "symfony/finder": "^3.4|^4.0|^5.0"
-            },
-            "require-dev": {
-                "php-mock/php-mock-phpunit": "^2.6",
-                "phpunit/phpunit": "^7.0|^8.0|^9.0",
-                "symfony/thanks": "^1.2"
-            },
-            "bin": [
-                "unused_scanner"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-legacy": "1.3.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "insolita\\Scanner\\Lib\\": "Lib",
-                    "insolita\\Scanner\\Exceptions\\": "Exceptions",
-                    "tests\\": "tests"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "mit"
-            ],
-            "authors": [
-                {
-                    "name": "insolita",
-                    "email": "webmaster100500@ya.ru"
-                }
-            ],
-            "description": "Detect unused composer dependencies",
-            "support": {
-                "issues": "https://github.com/Insolita/unused-scanner/issues",
-                "source": "https://github.com/Insolita/unused-scanner/tree/2.3.0"
-            },
-            "time": "2021-04-15T15:59:31+00:00"
-        },
-        {
-            "name": "justinrainbow/json-schema",
-            "version": "5.2.11",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/justinrainbow/json-schema.git",
-                "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa",
-                "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "require-dev": {
-                "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
-                "json-schema/json-schema-test-suite": "1.2.0",
-                "phpunit/phpunit": "^4.8.35"
-            },
-            "bin": [
-                "bin/validate-json"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "JsonSchema\\": "src/JsonSchema/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Bruno Prieto Reis",
-                    "email": "bruno.p.reis@gmail.com"
-                },
-                {
-                    "name": "Justin Rainbow",
-                    "email": "justin.rainbow@gmail.com"
-                },
-                {
-                    "name": "Igor Wiedler",
-                    "email": "igor@wiedler.ch"
-                },
-                {
-                    "name": "Robert Schönthal",
-                    "email": "seroscho@googlemail.com"
-                }
-            ],
-            "description": "A library to validate a json schema.",
-            "homepage": "https://github.com/justinrainbow/json-schema",
-            "keywords": [
-                "json",
-                "schema"
-            ],
-            "support": {
-                "issues": "https://github.com/justinrainbow/json-schema/issues",
-                "source": "https://github.com/justinrainbow/json-schema/tree/5.2.11"
-            },
-            "time": "2021-07-22T09:24:00+00:00"
-        },
-        {
-            "name": "kelunik/certificate",
-            "version": "v1.1.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/kelunik/certificate.git",
-                "reference": "56542e62d51533d04d0a9713261fea546bff80f6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/kelunik/certificate/zipball/56542e62d51533d04d0a9713261fea546bff80f6",
-                "reference": "56542e62d51533d04d0a9713261fea546bff80f6",
-                "shasum": ""
-            },
-            "require": {
-                "ext-openssl": "*",
-                "php": ">=5.4"
-            },
-            "require-dev": {
-                "fabpot/php-cs-fixer": "^1.9",
-                "phpunit/phpunit": "^4.8"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Kelunik\\Certificate\\": "lib"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Niklas Keller",
-                    "email": "me@kelunik.com"
-                }
-            ],
-            "description": "Access certificate details and transform between different formats.",
-            "keywords": [
-                "DER",
-                "certificate",
-                "certificates",
-                "openssl",
-                "pem",
-                "x509"
-            ],
-            "support": {
-                "issues": "https://github.com/kelunik/certificate/issues",
-                "source": "https://github.com/kelunik/certificate/tree/v1.1.2"
-            },
-            "time": "2019-05-29T19:02:31+00:00"
-        },
-        {
-            "name": "league/climate",
-            "version": "3.6.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/thephpleague/climate.git",
-                "reference": "61eecbde7645528a855a3afef65c5354093b27b2"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/climate/zipball/61eecbde7645528a855a3afef65c5354093b27b2",
-                "reference": "61eecbde7645528a855a3afef65c5354093b27b2",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2",
-                "psr/log": "^1.0",
-                "seld/cli-prompt": "^1.0"
-            },
-            "require-dev": {
-                "mikey179/vfsstream": "^1.4",
-                "mockery/mockery": "^1.0",
-                "phpunit/phpunit": "^5.7.16"
-            },
-            "suggest": {
-                "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "League\\CLImate\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Joe Tannenbaum",
-                    "email": "hey@joe.codes",
-                    "homepage": "http://joe.codes/",
-                    "role": "Developer"
-                },
-                {
-                    "name": "Craig Duncan",
-                    "email": "git@duncanc.co.uk",
-                    "homepage": "https://github.com/duncan3dc",
-                    "role": "Developer"
-                }
-            ],
-            "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.",
-            "keywords": [
-                "cli",
-                "colors",
-                "command",
-                "php",
-                "terminal"
-            ],
-            "support": {
-                "issues": "https://github.com/thephpleague/climate/issues",
-                "source": "https://github.com/thephpleague/climate/tree/3.6.0"
-            },
-            "time": "2020-10-04T16:02:59+00:00"
-        },
-        {
-            "name": "league/uri-parser",
-            "version": "1.4.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/thephpleague/uri-parser.git",
-                "reference": "671548427e4c932352d9b9279fdfa345bf63fa00"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/uri-parser/zipball/671548427e4c932352d9b9279fdfa345bf63fa00",
-                "reference": "671548427e4c932352d9b9279fdfa345bf63fa00",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.0.0"
-            },
-            "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.0",
-                "phpstan/phpstan": "^0.9.2",
-                "phpstan/phpstan-phpunit": "^0.9.4",
-                "phpstan/phpstan-strict-rules": "^0.9.0",
-                "phpunit/phpunit": "^6.0"
-            },
-            "suggest": {
-                "ext-intl": "Allow parsing RFC3987 compliant hosts",
-                "league/uri-schemes": "Allow validating and normalizing URI parsing results"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "League\\Uri\\": "src"
-                },
-                "files": [
-                    "src/functions_include.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Ignace Nyamagana Butera",
-                    "email": "nyamsprod@gmail.com",
-                    "homepage": "https://nyamsprod.com"
-                }
-            ],
-            "description": "userland URI parser RFC 3986 compliant",
-            "homepage": "https://github.com/thephpleague/uri-parser",
-            "keywords": [
-                "parse_url",
-                "parser",
-                "rfc3986",
-                "rfc3987",
-                "uri",
-                "url"
-            ],
-            "support": {
-                "issues": "https://github.com/thephpleague/uri-parser/issues",
-                "source": "https://github.com/thephpleague/uri-parser/tree/master"
-            },
-            "time": "2018-11-22T07:55:51+00:00"
-        },
-        {
-            "name": "localheinz/diff",
-            "version": "1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/localheinz/diff.git",
-                "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/localheinz/diff/zipball/851bb20ea8358c86f677f5f111c4ab031b1c764c",
-                "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1 || ^8.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^7.5 || ^8.0",
-                "symfony/process": "^4.2 || ^5"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                }
-            ],
-            "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize",
-            "homepage": "https://github.com/localheinz/diff",
-            "keywords": [
-                "diff",
-                "udiff",
-                "unidiff",
-                "unified diff"
-            ],
-            "support": {
-                "source": "https://github.com/localheinz/diff/tree/main"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-07-06T04:49:32+00:00"
-        },
-        {
-            "name": "myclabs/deep-copy",
-            "version": "1.10.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
-                "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1 || ^8.0"
-            },
-            "replace": {
-                "myclabs/deep-copy": "self.version"
-            },
-            "require-dev": {
-                "doctrine/collections": "^1.0",
-                "doctrine/common": "^2.6",
-                "phpunit/phpunit": "^7.1"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "DeepCopy\\": "src/DeepCopy/"
-                },
-                "files": [
-                    "src/DeepCopy/deep_copy.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "description": "Create deep copies (clones) of your objects",
-            "keywords": [
-                "clone",
-                "copy",
-                "duplicate",
-                "object",
-                "object graph"
-            ],
-            "support": {
-                "issues": "https://github.com/myclabs/DeepCopy/issues",
-                "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
-            },
-            "funding": [
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-11-13T09:40:50+00:00"
-        },
-        {
-            "name": "netresearch/jsonmapper",
-            "version": "v4.0.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/cweiske/jsonmapper.git",
-                "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d",
-                "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "ext-pcre": "*",
-                "ext-reflection": "*",
-                "ext-spl": "*",
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0",
-                "squizlabs/php_codesniffer": "~3.5"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "JsonMapper": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "OSL-3.0"
-            ],
-            "authors": [
-                {
-                    "name": "Christian Weiske",
-                    "email": "cweiske@cweiske.de",
-                    "homepage": "http://github.com/cweiske/jsonmapper/",
-                    "role": "Developer"
-                }
-            ],
-            "description": "Map nested JSON structures onto PHP classes",
-            "support": {
-                "email": "cweiske@cweiske.de",
-                "issues": "https://github.com/cweiske/jsonmapper/issues",
-                "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0"
-            },
-            "time": "2020-12-01T19:48:11+00:00"
-        },
-        {
-            "name": "nikic/php-parser",
-            "version": "v4.13.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "50953a2691a922aa1769461637869a0a2faa3f53"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53",
-                "reference": "50953a2691a922aa1769461637869a0a2faa3f53",
-                "shasum": ""
-            },
-            "require": {
-                "ext-tokenizer": "*",
-                "php": ">=7.0"
-            },
-            "require-dev": {
-                "ircmaxell/php-yacc": "^0.0.7",
-                "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
-            },
-            "bin": [
-                "bin/php-parse"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.9-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "PhpParser\\": "lib/PhpParser"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Nikita Popov"
-                }
-            ],
-            "description": "A PHP parser written in PHP",
-            "keywords": [
-                "parser",
-                "php"
-            ],
-            "support": {
-                "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0"
-            },
-            "time": "2021-09-20T12:20:58+00:00"
-        },
-        {
-            "name": "openlss/lib-array2xml",
-            "version": "1.0.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/nullivex/lib-array2xml.git",
-                "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90",
-                "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "LSS": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "Apache-2.0"
-            ],
-            "authors": [
-                {
-                    "name": "Bryan Tong",
-                    "email": "bryan@nullivex.com",
-                    "homepage": "https://www.nullivex.com"
-                },
-                {
-                    "name": "Tony Butler",
-                    "email": "spudz76@gmail.com",
-                    "homepage": "https://www.nullivex.com"
-                }
-            ],
-            "description": "Array2XML conversion library credit to lalit.org",
-            "homepage": "https://www.nullivex.com",
-            "keywords": [
-                "array",
-                "array conversion",
-                "xml",
-                "xml conversion"
-            ],
-            "support": {
-                "issues": "https://github.com/nullivex/lib-array2xml/issues",
-                "source": "https://github.com/nullivex/lib-array2xml/tree/master"
-            },
-            "time": "2019-03-29T20:06:56+00:00"
-        },
-        {
-            "name": "pdepend/pdepend",
-            "version": "2.10.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/pdepend/pdepend.git",
-                "reference": "1fd30f4352b630ad53fec3fd5e8b8ba760f85596"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1fd30f4352b630ad53fec3fd5e8b8ba760f85596",
-                "reference": "1fd30f4352b630ad53fec3fd5e8b8ba760f85596",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.7",
-                "symfony/config": "^2.3.0|^3|^4|^5",
-                "symfony/dependency-injection": "^2.3.0|^3|^4|^5",
-                "symfony/filesystem": "^2.3.0|^3|^4|^5"
-            },
-            "require-dev": {
-                "easy-doc/easy-doc": "0.0.0|^1.2.3",
-                "gregwar/rst": "^1.0",
-                "phpunit/phpunit": "^4.8.36|^5.7.27",
-                "squizlabs/php_codesniffer": "^2.0.0"
-            },
-            "bin": [
-                "src/bin/pdepend"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "PDepend\\": "src/main/php/PDepend"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "description": "Official version of pdepend to be handled with Composer",
-            "support": {
-                "issues": "https://github.com/pdepend/pdepend/issues",
-                "source": "https://github.com/pdepend/pdepend/tree/2.10.0"
-            },
-            "funding": [
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-20T09:56:09+00:00"
-        },
-        {
-            "name": "phar-io/manifest",
-            "version": "2.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phar-io/manifest.git",
-                "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
-                "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-phar": "*",
-                "ext-xmlwriter": "*",
-                "phar-io/version": "^3.0.1",
-                "php": "^7.2 || ^8.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
-                },
-                {
-                    "name": "Sebastian Heuer",
-                    "email": "sebastian@phpeople.de",
-                    "role": "Developer"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "Developer"
-                }
-            ],
-            "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
-            "support": {
-                "issues": "https://github.com/phar-io/manifest/issues",
-                "source": "https://github.com/phar-io/manifest/tree/2.0.3"
-            },
-            "time": "2021-07-20T11:28:43+00:00"
-        },
-        {
-            "name": "phar-io/version",
-            "version": "3.1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phar-io/version.git",
-                "reference": "bae7c545bef187884426f042434e561ab1ddb182"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
-                "reference": "bae7c545bef187884426f042434e561ab1ddb182",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
-                },
-                {
-                    "name": "Sebastian Heuer",
-                    "email": "sebastian@phpeople.de",
-                    "role": "Developer"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "Developer"
-                }
-            ],
-            "description": "Library for handling version information and constraints",
-            "support": {
-                "issues": "https://github.com/phar-io/version/issues",
-                "source": "https://github.com/phar-io/version/tree/3.1.0"
-            },
-            "time": "2021-02-23T14:00:09+00:00"
-        },
-        {
-            "name": "php-cs-fixer/diff",
-            "version": "v2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/PHP-CS-Fixer/diff.git",
-                "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3",
-                "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.6 || ^7.0 || ^8.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
-                "symfony/process": "^3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                }
-            ],
-            "description": "sebastian/diff v3 backport support for PHP 5.6+",
-            "homepage": "https://github.com/PHP-CS-Fixer",
-            "keywords": [
-                "diff"
-            ],
-            "support": {
-                "issues": "https://github.com/PHP-CS-Fixer/diff/issues",
-                "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2"
-            },
-            "time": "2020-10-14T08:32:19+00:00"
-        },
-        {
-            "name": "php-parallel-lint/php-parallel-lint",
-            "version": "v1.3.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git",
-                "reference": "761f3806e30239b5fcd90a0a45d41dc2138de192"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/761f3806e30239b5fcd90a0a45d41dc2138de192",
-                "reference": "761f3806e30239b5fcd90a0a45d41dc2138de192",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "php": ">=5.3.0"
-            },
-            "replace": {
-                "grogy/php-parallel-lint": "*",
-                "jakub-onderka/php-parallel-lint": "*"
-            },
-            "require-dev": {
-                "nette/tester": "^1.3 || ^2.0",
-                "php-parallel-lint/php-console-highlighter": "~0.3",
-                "squizlabs/php_codesniffer": "^3.6"
-            },
-            "suggest": {
-                "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet"
-            },
-            "bin": [
-                "parallel-lint"
-            ],
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "./"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-2-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Jakub Onderka",
-                    "email": "ahoj@jakubonderka.cz"
-                }
-            ],
-            "description": "This tool check syntax of PHP files about 20x faster than serial check.",
-            "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
-            "support": {
-                "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues",
-                "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.3.1"
-            },
-            "time": "2021-08-13T05:35:13+00:00"
-        },
-        {
-            "name": "phpdocumentor/reflection-common",
-            "version": "2.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
-                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-2.x": "2.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Jaap van Otterdijk",
-                    "email": "opensource@ijaap.nl"
-                }
-            ],
-            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
-            "homepage": "http://www.phpdoc.org",
-            "keywords": [
-                "FQSEN",
-                "phpDocumentor",
-                "phpdoc",
-                "reflection",
-                "static analysis"
-            ],
-            "support": {
-                "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
-                "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
-            },
-            "time": "2020-06-27T09:03:43+00:00"
-        },
-        {
-            "name": "phpdocumentor/reflection-docblock",
-            "version": "5.2.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
-                "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
-                "shasum": ""
-            },
-            "require": {
-                "ext-filter": "*",
-                "php": "^7.2 || ^8.0",
-                "phpdocumentor/reflection-common": "^2.2",
-                "phpdocumentor/type-resolver": "^1.3",
-                "webmozart/assert": "^1.9.1"
-            },
-            "require-dev": {
-                "mockery/mockery": "~1.3.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Mike van Riel",
-                    "email": "me@mikevanriel.com"
-                },
-                {
-                    "name": "Jaap van Otterdijk",
-                    "email": "account@ijaap.nl"
-                }
-            ],
-            "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "support": {
-                "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
-                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
-            },
-            "time": "2020-09-03T19:13:55+00:00"
-        },
-        {
-            "name": "phpdocumentor/type-resolver",
-            "version": "1.5.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae",
-                "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0",
-                "phpdocumentor/reflection-common": "^2.0"
-            },
-            "require-dev": {
-                "ext-tokenizer": "*",
-                "psalm/phar": "^4.8"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-1.x": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Mike van Riel",
-                    "email": "me@mikevanriel.com"
-                }
-            ],
-            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
-            "support": {
-                "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
-                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1"
-            },
-            "time": "2021-10-02T14:08:47+00:00"
-        },
-        {
-            "name": "phpmd/phpmd",
-            "version": "2.10.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpmd/phpmd.git",
-                "reference": "1bc74db7cf834662d83abebae265be11bb2eec3a"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpmd/phpmd/zipball/1bc74db7cf834662d83abebae265be11bb2eec3a",
-                "reference": "1bc74db7cf834662d83abebae265be11bb2eec3a",
-                "shasum": ""
-            },
-            "require": {
-                "composer/xdebug-handler": "^1.0 || ^2.0",
-                "ext-xml": "*",
-                "pdepend/pdepend": "^2.10.0",
-                "php": ">=5.3.9"
-            },
-            "require-dev": {
-                "easy-doc/easy-doc": "0.0.0 || ^1.3.2",
-                "ext-json": "*",
-                "ext-simplexml": "*",
-                "gregwar/rst": "^1.0",
-                "mikey179/vfsstream": "^1.6.8",
-                "phpunit/phpunit": "^4.8.36 || ^5.7.27",
-                "squizlabs/php_codesniffer": "^2.0"
-            },
-            "bin": [
-                "src/bin/phpmd"
-            ],
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "PHPMD\\": "src/main/php"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Manuel Pichler",
-                    "email": "github@manuel-pichler.de",
-                    "homepage": "https://github.com/manuelpichler",
-                    "role": "Project Founder"
-                },
-                {
-                    "name": "Marc Würth",
-                    "email": "ravage@bluewin.ch",
-                    "homepage": "https://github.com/ravage84",
-                    "role": "Project Maintainer"
-                },
-                {
-                    "name": "Other contributors",
-                    "homepage": "https://github.com/phpmd/phpmd/graphs/contributors",
-                    "role": "Contributors"
-                }
-            ],
-            "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
-            "homepage": "https://phpmd.org/",
-            "keywords": [
-                "mess detection",
-                "mess detector",
-                "pdepend",
-                "phpmd",
-                "pmd"
-            ],
-            "support": {
-                "irc": "irc://irc.freenode.org/phpmd",
-                "issues": "https://github.com/phpmd/phpmd/issues",
-                "source": "https://github.com/phpmd/phpmd/tree/2.10.2"
-            },
-            "funding": [
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-22T09:56:23+00:00"
-        },
-        {
-            "name": "phpspec/prophecy",
-            "version": "1.14.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
-                "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.2",
-                "php": "^7.2 || ~8.0, <8.2",
-                "phpdocumentor/reflection-docblock": "^5.2",
-                "sebastian/comparator": "^3.0 || ^4.0",
-                "sebastian/recursion-context": "^3.0 || ^4.0"
-            },
-            "require-dev": {
-                "phpspec/phpspec": "^6.0 || ^7.0",
-                "phpunit/phpunit": "^8.0 || ^9.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Prophecy\\": "src/Prophecy"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Konstantin Kudryashov",
-                    "email": "ever.zet@gmail.com",
-                    "homepage": "http://everzet.com"
-                },
-                {
-                    "name": "Marcello Duarte",
-                    "email": "marcello.duarte@gmail.com"
-                }
-            ],
-            "description": "Highly opinionated mocking framework for PHP 5.3+",
-            "homepage": "https://github.com/phpspec/prophecy",
-            "keywords": [
-                "Double",
-                "Dummy",
-                "fake",
-                "mock",
-                "spy",
-                "stub"
-            ],
-            "support": {
-                "issues": "https://github.com/phpspec/prophecy/issues",
-                "source": "https://github.com/phpspec/prophecy/tree/1.14.0"
-            },
-            "time": "2021-09-10T09:02:12+00:00"
-        },
-        {
-            "name": "phpstan/phpstan",
-            "version": "0.12.99",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4d40f1d759942f523be267a1bab6884f46ca3f7",
-                "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1|^8.0"
-            },
-            "conflict": {
-                "phpstan/phpstan-shim": "*"
-            },
-            "bin": [
-                "phpstan",
-                "phpstan.phar"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "0.12-dev"
-                }
-            },
-            "autoload": {
-                "files": [
-                    "bootstrap.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "description": "PHPStan - PHP Static Analysis Tool",
-            "support": {
-                "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/0.12.99"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/ondrejmirtes",
-                    "type": "github"
-                },
-                {
-                    "url": "https://github.com/phpstan",
-                    "type": "github"
-                },
-                {
-                    "url": "https://www.patreon.com/phpstan",
-                    "type": "patreon"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-09-12T20:09:55+00:00"
-        },
-        {
-            "name": "phpunit/php-code-coverage",
-            "version": "7.0.15",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "819f92bba8b001d4363065928088de22f25a3a48"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/819f92bba8b001d4363065928088de22f25a3a48",
-                "reference": "819f92bba8b001d4363065928088de22f25a3a48",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-xmlwriter": "*",
-                "php": ">=7.2",
-                "phpunit/php-file-iterator": "^2.0.2",
-                "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^3.1.3 || ^4.0",
-                "sebastian/code-unit-reverse-lookup": "^1.0.1",
-                "sebastian/environment": "^4.2.2",
-                "sebastian/version": "^2.0.1",
-                "theseer/tokenizer": "^1.1.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.2.2"
-            },
-            "suggest": {
-                "ext-xdebug": "^2.7.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "7.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
-            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
-            "keywords": [
-                "coverage",
-                "testing",
-                "xunit"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-07-26T12:20:09+00:00"
-        },
-        {
-            "name": "phpunit/php-file-iterator",
-            "version": "2.0.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "28af674ff175d0768a5a978e6de83f697d4a7f05"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/28af674ff175d0768a5a978e6de83f697d4a7f05",
-                "reference": "28af674ff175d0768a5a978e6de83f697d4a7f05",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.5"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
-            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
-            "keywords": [
-                "filesystem",
-                "iterator"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
-                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.4"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-07-19T06:46:01+00:00"
-        },
-        {
-            "name": "phpunit/php-text-template",
-            "version": "1.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-text-template.git",
-                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
-                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Simple template engine.",
-            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
-            "keywords": [
-                "template"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
-                "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
-            },
-            "time": "2015-06-21T13:50:34+00:00"
-        },
-        {
-            "name": "phpunit/php-timer",
-            "version": "2.1.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
-                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.5"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.1-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Utility class for timing",
-            "homepage": "https://github.com/sebastianbergmann/php-timer/",
-            "keywords": [
-                "timer"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/php-timer/issues",
-                "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T08:20:02+00:00"
-        },
-        {
-            "name": "phpunit/php-token-stream",
-            "version": "3.1.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768",
-                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768",
-                "shasum": ""
-            },
-            "require": {
-                "ext-tokenizer": "*",
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^7.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Wrapper around PHP's tokenizer extension.",
-            "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
-            "keywords": [
-                "tokenizer"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
-                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "abandoned": true,
-            "time": "2021-07-26T12:15:06+00:00"
-        },
-        {
-            "name": "phpunit/phpunit",
-            "version": "8.5.21",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "50a58a60b85947b0bee4c8ecfe0f4bbdcf20e984"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/50a58a60b85947b0bee4c8ecfe0f4bbdcf20e984",
-                "reference": "50a58a60b85947b0bee4c8ecfe0f4bbdcf20e984",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.3.1",
-                "ext-dom": "*",
-                "ext-json": "*",
-                "ext-libxml": "*",
-                "ext-mbstring": "*",
-                "ext-xml": "*",
-                "ext-xmlwriter": "*",
-                "myclabs/deep-copy": "^1.10.0",
-                "phar-io/manifest": "^2.0.3",
-                "phar-io/version": "^3.0.2",
-                "php": ">=7.2",
-                "phpspec/prophecy": "^1.10.3",
-                "phpunit/php-code-coverage": "^7.0.12",
-                "phpunit/php-file-iterator": "^2.0.4",
-                "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-timer": "^2.1.2",
-                "sebastian/comparator": "^3.0.2",
-                "sebastian/diff": "^3.0.2",
-                "sebastian/environment": "^4.2.3",
-                "sebastian/exporter": "^3.1.2",
-                "sebastian/global-state": "^3.0.0",
-                "sebastian/object-enumerator": "^3.0.3",
-                "sebastian/resource-operations": "^2.0.1",
-                "sebastian/type": "^1.1.3",
-                "sebastian/version": "^2.0.1"
-            },
-            "require-dev": {
-                "ext-pdo": "*"
-            },
-            "suggest": {
-                "ext-soap": "*",
-                "ext-xdebug": "*",
-                "phpunit/php-invoker": "^2.0.0"
-            },
-            "bin": [
-                "phpunit"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "8.5-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "The PHP Unit Testing framework.",
-            "homepage": "https://phpunit.de/",
-            "keywords": [
-                "phpunit",
-                "testing",
-                "xunit"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.21"
-            },
-            "funding": [
-                {
-                    "url": "https://phpunit.de/donate.html",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-09-25T07:37:20+00:00"
-        },
-        {
-            "name": "predis/predis",
-            "version": "v1.1.8",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/predis/predis.git",
-                "reference": "cf5c118a077fbab8b9af1482c20952173125c041"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/predis/predis/zipball/cf5c118a077fbab8b9af1482c20952173125c041",
-                "reference": "cf5c118a077fbab8b9af1482c20952173125c041",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.9"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.8"
-            },
-            "suggest": {
-                "ext-curl": "Allows access to Webdis when paired with phpiredis",
-                "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Predis\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Daniele Alessandri",
-                    "email": "suppakilla@gmail.com",
-                    "homepage": "http://clorophilla.net",
-                    "role": "Creator & Maintainer"
-                },
-                {
-                    "name": "Till Krüss",
-                    "homepage": "https://till.im",
-                    "role": "Maintainer"
-                }
-            ],
-            "description": "Flexible and feature-complete Redis client for PHP and HHVM",
-            "homepage": "http://github.com/predis/predis",
-            "keywords": [
-                "nosql",
-                "predis",
-                "redis"
-            ],
-            "support": {
-                "issues": "https://github.com/predis/predis/issues",
-                "source": "https://github.com/predis/predis/tree/v1.1.8"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sponsors/tillkruss",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-09-29T17:48:39+00:00"
-        },
-        {
-            "name": "psr/cache",
-            "version": "1.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/cache.git",
-                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
-                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Cache\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "http://www.php-fig.org/"
-                }
-            ],
-            "description": "Common interface for caching libraries",
-            "keywords": [
-                "cache",
-                "psr",
-                "psr-6"
-            ],
-            "support": {
-                "source": "https://github.com/php-fig/cache/tree/master"
-            },
-            "time": "2016-08-06T20:24:11+00:00"
-        },
-        {
-            "name": "psr/container",
-            "version": "1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/container.git",
-                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
-                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.2.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Container\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "https://www.php-fig.org/"
-                }
-            ],
-            "description": "Common Container Interface (PHP FIG PSR-11)",
-            "homepage": "https://github.com/php-fig/container",
-            "keywords": [
-                "PSR-11",
-                "container",
-                "container-interface",
-                "container-interop",
-                "psr"
-            ],
-            "support": {
-                "issues": "https://github.com/php-fig/container/issues",
-                "source": "https://github.com/php-fig/container/tree/1.1.1"
-            },
-            "time": "2021-03-05T17:36:06+00:00"
-        },
-        {
-            "name": "psr/http-message",
-            "version": "1.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/http-message.git",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Http\\Message\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "http://www.php-fig.org/"
-                }
-            ],
-            "description": "Common interface for HTTP messages",
-            "homepage": "https://github.com/php-fig/http-message",
-            "keywords": [
-                "http",
-                "http-message",
-                "psr",
-                "psr-7",
-                "request",
-                "response"
-            ],
-            "support": {
-                "source": "https://github.com/php-fig/http-message/tree/master"
-            },
-            "time": "2016-08-06T14:39:51+00:00"
-        },
-        {
-            "name": "psr/log",
-            "version": "1.1.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/log.git",
-                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
-                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Log\\": "Psr/Log/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "https://www.php-fig.org/"
-                }
-            ],
-            "description": "Common interface for logging libraries",
-            "homepage": "https://github.com/php-fig/log",
-            "keywords": [
-                "log",
-                "psr",
-                "psr-3"
-            ],
-            "support": {
-                "source": "https://github.com/php-fig/log/tree/1.1.4"
-            },
-            "time": "2021-05-03T11:20:27+00:00"
-        },
-        {
-            "name": "ptrofimov/tinyredisclient",
-            "version": "1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ptrofimov/tinyredisclient.git",
-                "reference": "7b8d487afa41b74e30d1a68400d89c12d9b82b6f"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ptrofimov/tinyredisclient/zipball/7b8d487afa41b74e30d1a68400d89c12d9b82b6f",
-                "reference": "7b8d487afa41b74e30d1a68400d89c12d9b82b6f",
-                "shasum": ""
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Petr Trofimov",
-                    "email": "petrofimov@yandex.ru",
-                    "homepage": "https://github.com/ptrofimov"
-                }
-            ],
-            "description": "The most lightweight Redis client written in PHP",
-            "homepage": "https://github.com/ptrofimov/tinyredisclient",
-            "keywords": [
-                "client",
-                "redis"
-            ],
-            "support": {
-                "issues": "https://github.com/ptrofimov/tinyredisclient/issues",
-                "source": "https://github.com/ptrofimov/tinyredisclient/tree/master"
-            },
-            "time": "2013-04-09T19:16:58+00:00"
-        },
-        {
-            "name": "ralouphie/getallheaders",
-            "version": "3.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ralouphie/getallheaders.git",
-                "reference": "120b605dfeb996808c31b6477290a714d356e822"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
-                "reference": "120b605dfeb996808c31b6477290a714d356e822",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.6"
-            },
-            "require-dev": {
-                "php-coveralls/php-coveralls": "^2.1",
-                "phpunit/phpunit": "^5 || ^6.5"
-            },
-            "type": "library",
-            "autoload": {
-                "files": [
-                    "src/getallheaders.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Ralph Khattar",
-                    "email": "ralph.khattar@gmail.com"
-                }
-            ],
-            "description": "A polyfill for getallheaders.",
-            "support": {
-                "issues": "https://github.com/ralouphie/getallheaders/issues",
-                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
-            },
-            "time": "2019-03-08T08:55:37+00:00"
-        },
-        {
-            "name": "redisent/redisent",
-            "version": "dev-master",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/jdp/redisent.git",
-                "reference": "0a9e40710153a5f36fbafe7d1f7fe0de3f723be4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/jdp/redisent/zipball/0a9e40710153a5f36fbafe7d1f7fe0de3f723be4",
-                "reference": "0a9e40710153a5f36fbafe7d1f7fe0de3f723be4",
-                "shasum": ""
-            },
-            "require": {
-                "ext-sockets": "*",
-                "php": ">=5.3.0"
-            },
-            "default-branch": true,
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "redisent": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "ISC"
-            ],
-            "authors": [
-                {
-                    "name": "Justin Poliey",
-                    "homepage": "http://justinpoliey.com/"
-                }
-            ],
-            "description": "Redisent",
-            "homepage": "https://github.com/jdp/redisent",
-            "keywords": [
-                "redis"
-            ],
-            "support": {
-                "issues": "https://github.com/jdp/redisent/issues",
-                "source": "https://github.com/jdp/redisent/tree/master"
-            },
-            "time": "2013-11-19T04:55:46+00:00"
-        },
-        {
-            "name": "roave/security-advisories",
-            "version": "dev-latest",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Roave/SecurityAdvisories.git",
-                "reference": "0488e161600117fc3a0d72397dad154729002f54"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0488e161600117fc3a0d72397dad154729002f54",
-                "reference": "0488e161600117fc3a0d72397dad154729002f54",
-                "shasum": ""
-            },
-            "conflict": {
-                "3f/pygmentize": "<1.2",
-                "adodb/adodb-php": "<5.20.12",
-                "akaunting/akaunting": "<2.1.13",
-                "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
-                "amazing/media2click": ">=1,<1.3.3",
-                "amphp/artax": "<1.0.6|>=2,<2.0.6",
-                "amphp/http": "<1.0.1",
-                "amphp/http-client": ">=4,<4.4",
-                "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
-                "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
-                "aws/aws-sdk-php": ">=3,<3.2.1",
-                "bagisto/bagisto": "<0.1.5",
-                "barrelstrength/sprout-base-email": "<1.2.7",
-                "barrelstrength/sprout-forms": "<3.9",
-                "baserproject/basercms": "<=4.5",
-                "billz/raspap-webgui": "<=2.6.6",
-                "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
-                "bolt/bolt": "<3.7.2",
-                "bolt/core": "<4.1.13",
-                "brightlocal/phpwhois": "<=4.2.5",
-                "buddypress/buddypress": "<5.1.2",
-                "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
-                "cachethq/cachet": "<2.5.1",
-                "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
-                "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
-                "cartalyst/sentry": "<=2.1.6",
-                "centreon/centreon": "<20.10.7",
-                "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
-                "codeception/codeception": "<3.1.3|>=4,<4.1.22",
-                "codeigniter/framework": "<=3.0.6",
-                "codiad/codiad": "<=2.8.4",
-                "composer/composer": "<1.10.22|>=2-alpha.1,<2.0.13",
-                "contao-components/mediaelement": ">=2.14.2,<2.21.1",
-                "contao/core": ">=2,<3.5.39",
-                "contao/core-bundle": ">=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|= 4.10.0",
-                "contao/listing-bundle": ">=4,<4.4.8",
-                "craftcms/cms": "<3.6.7",
-                "croogo/croogo": "<3.0.7",
-                "datadog/dd-trace": ">=0.30,<0.30.2",
-                "david-garcia/phpwhois": "<=4.3.1",
-                "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1",
-                "directmailteam/direct-mail": "<5.2.4",
-                "doctrine/annotations": ">=1,<1.2.7",
-                "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2",
-                "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1",
-                "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2",
-                "doctrine/doctrine-bundle": "<1.5.2",
-                "doctrine/doctrine-module": "<=0.7.1",
-                "doctrine/mongodb-odm": ">=1,<1.0.2",
-                "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
-                "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
-                "dolibarr/dolibarr": "<14|>= 3.3.beta1, < 13.0.2",
-                "dompdf/dompdf": ">=0.6,<0.6.2",
-                "drupal/core": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4",
-                "drupal/drupal": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4",
-                "dweeves/magmi": "<=0.7.24",
-                "ecodev/newsletter": "<=4",
-                "endroid/qr-code-bundle": "<3.4.2",
-                "enshrined/svg-sanitize": "<0.13.1",
-                "erusev/parsedown": "<1.7.2",
-                "ether/logs": "<3.0.4",
-                "ezsystems/demobundle": ">=5.4,<5.4.6.1",
-                "ezsystems/ez-support-tools": ">=2.2,<2.2.3",
-                "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
-                "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
-                "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24",
-                "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
-                "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
-                "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<=1.3.1",
-                "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
-                "ezsystems/ezplatform-user": ">=1,<1.0.1",
-                "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<=7.5.15.1",
-                "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.3.5.1",
-                "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
-                "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
-                "ezyang/htmlpurifier": "<4.1.1",
-                "facade/ignition": "<1.16.14|>=2,<2.4.2|>=2.5,<2.5.2",
-                "feehi/cms": "<=2.1.1",
-                "feehi/feehicms": "<=0.1.3",
-                "firebase/php-jwt": "<2",
-                "flarum/core": ">=1,<=1.0.1",
-                "flarum/sticky": ">=0.1-beta.14,<=0.1-beta.15",
-                "flarum/tags": "<=0.1-beta.13",
-                "fluidtypo3/vhs": "<5.1.1",
-                "fooman/tcpdf": "<6.2.22",
-                "forkcms/forkcms": "<=5.9.2",
-                "fossar/tcpdf-parser": "<6.2.22",
-                "francoisjacquet/rosariosis": "<6.5.1",
-                "friendsofsymfony/oauth2-php": "<1.3",
-                "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
-                "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
-                "friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
-                "froala/wysiwyg-editor": "<3.2.7",
-                "fuel/core": "<1.8.1",
-                "getgrav/grav": "<1.7.21",
-                "getkirby/cms": "<=3.5.6",
-                "getkirby/panel": "<2.5.14",
-                "gilacms/gila": "<=1.11.4",
-                "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
-                "gree/jose": "<=2.2",
-                "gregwar/rst": "<1.0.3",
-                "grumpydictator/firefly-iii": "<5.6.1",
-                "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
-                "helloxz/imgurl": "<=2.31",
-                "ibexa/post-install": "<=1.0.4",
-                "icecoder/icecoder": "<=8",
-                "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10",
-                "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4",
-                "illuminate/database": "<6.20.26|>=7,<8.40",
-                "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
-                "illuminate/view": ">=7,<7.1.2",
-                "impresscms/impresscms": "<=1.4.2",
-                "in2code/femanager": "<5.5.1|>=6,<6.3.1",
-                "intelliants/subrion": "<=4.2.1",
-                "ivankristianto/phpwhois": "<=4.3",
-                "james-heinrich/getid3": "<1.9.9",
-                "joomla/archive": "<1.1.10",
-                "joomla/session": "<1.3.1",
-                "jsmitty12/phpwhois": "<5.1",
-                "kazist/phpwhois": "<=4.2.6",
-                "kitodo/presentation": "<3.1.2",
-                "klaviyo/magento2-extension": ">=1,<3",
-                "kreait/firebase-php": ">=3.2,<3.8.1",
-                "la-haute-societe/tcpdf": "<6.2.22",
-                "laminas/laminas-http": "<2.14.2",
-                "laravel/framework": "<6.20.26|>=7,<8.40",
-                "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
-                "lavalite/cms": "<=5.8",
-                "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
-                "league/commonmark": "<0.18.3",
-                "league/flysystem": "<1.1.4|>=2,<2.1.1",
-                "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
-                "librenms/librenms": "<21.1",
-                "livewire/livewire": ">2.2.4,<2.2.6",
-                "lms/routes": "<2.1.1",
-                "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2",
-                "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
-                "magento/magento1ce": "<1.9.4.3",
-                "magento/magento1ee": ">=1,<1.14.4.3",
-                "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
-                "marcwillmann/turn": "<0.3.3",
-                "mautic/core": "<4|= 2.13.1",
-                "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35",
-                "miniorange/miniorange-saml": "<1.4.3",
-                "mittwald/typo3_forum": "<1.2.1",
-                "monolog/monolog": ">=1.8,<1.12",
-                "moodle/moodle": "<3.5.17|>=3.7,<3.7.9|>=3.8,<3.8.8|>=3.9,<3.9.5|>=3.10,<3.10.2",
-                "namshi/jose": "<2.2",
-                "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
-                "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3",
-                "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
-                "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5",
-                "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
-                "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
-                "nilsteampassnet/teampass": "<=2.1.27.36",
-                "nukeviet/nukeviet": "<4.3.4",
-                "nystudio107/craft-seomatic": "<3.3",
-                "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
-                "october/backend": "<1.1.2",
-                "october/cms": "= 1.1.1|= 1.0.471|= 1.0.469|>=1.0.319,<1.0.469",
-                "october/october": ">=1.0.319,<1.0.466",
-                "october/rain": "<1.0.472|>=1.1,<1.1.2",
-                "october/system": "<1.0.472|>=1.1.1,<1.1.5",
-                "onelogin/php-saml": "<2.10.4",
-                "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
-                "opencart/opencart": "<=3.0.3.2",
-                "openid/php-openid": "<2.3",
-                "openmage/magento-lts": "<19.4.15|>=20,<20.0.13",
-                "orchid/platform": ">=9,<9.4.4",
-                "oro/crm": ">=1.7,<1.7.4",
-                "oro/platform": ">=1.7,<1.7.4",
-                "padraic/humbug_get_contents": "<1.1.2",
-                "pagarme/pagarme-php": ">=0,<3",
-                "pagekit/pagekit": "<=1.0.18",
-                "paragonie/random_compat": "<2",
-                "passbolt/passbolt_api": "<2.11",
-                "paypal/merchant-sdk-php": "<3.12",
-                "pear/archive_tar": "<1.4.14",
-                "personnummer/personnummer": "<3.0.2",
-                "phanan/koel": "<5.1.4",
-                "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7",
-                "phpmailer/phpmailer": "<6.5",
-                "phpmussel/phpmussel": ">=1,<1.6",
-                "phpmyadmin/phpmyadmin": "<4.9.6|>=5,<5.0.3",
-                "phpoffice/phpexcel": "<1.8.2",
-                "phpoffice/phpspreadsheet": "<1.16",
-                "phpseclib/phpseclib": "<2.0.31|>=3,<3.0.7",
-                "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
-                "phpwhois/phpwhois": "<=4.2.5",
-                "phpxmlrpc/extras": "<0.6.1",
-                "pimcore/pimcore": "<10.1.3",
-                "pocketmine/pocketmine-mp": "<3.15.4",
-                "pressbooks/pressbooks": "<5.18",
-                "prestashop/autoupgrade": ">=4,<4.10.1",
-                "prestashop/contactform": ">1.0.1,<4.3",
-                "prestashop/gamification": "<2.3.2",
-                "prestashop/productcomments": ">=4,<4.2.1",
-                "prestashop/ps_emailsubscription": "<2.6.1",
-                "prestashop/ps_facetedsearch": "<3.4.1",
-                "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
-                "propel/propel": ">=2-alpha.1,<=2-alpha.7",
-                "propel/propel1": ">=1,<=1.7.1",
-                "pterodactyl/panel": "<0.7.19|>=1-rc.0,<=1-rc.6",
-                "pusher/pusher-php-server": "<2.2.1",
-                "pwweb/laravel-core": "<=0.3.6-beta",
-                "rainlab/debugbar-plugin": "<3.1",
-                "rmccue/requests": ">=1.6,<1.8",
-                "robrichards/xmlseclibs": "<3.0.4",
-                "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1",
-                "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
-                "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
-                "sensiolabs/connect": "<4.2.3",
-                "serluck/phpwhois": "<=4.2.6",
-                "shopware/core": "<=6.4.3",
-                "shopware/platform": "<=6.4.3",
-                "shopware/production": "<=6.3.5.2",
-                "shopware/shopware": "<5.6.10",
-                "showdoc/showdoc": "<=2.9.8",
-                "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
-                "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
-                "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
-                "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
-                "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
-                "silverstripe/framework": "<4.7.4",
-                "silverstripe/graphql": "<=3.5|>=4-alpha.1,<4-alpha.2",
-                "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
-                "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
-                "silverstripe/subsites": ">=2,<2.1.1",
-                "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
-                "silverstripe/userforms": "<3",
-                "simple-updates/phpwhois": "<=1",
-                "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
-                "simplesamlphp/simplesamlphp": "<1.18.6",
-                "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
-                "simplito/elliptic-php": "<1.0.6",
-                "slim/slim": "<2.6",
-                "smarty/smarty": "<3.1.39",
-                "socalnick/scn-social-auth": "<1.15.2",
-                "socialiteproviders/steam": "<1.1",
-                "spoonity/tcpdf": "<6.2.22",
-                "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
-                "ssddanbrown/bookstack": "<0.29.2",
-                "stormpath/sdk": ">=0,<9.9.99",
-                "studio-42/elfinder": "<2.1.59",
-                "subrion/cms": "<=4.2.1",
-                "sulu/sulu": "<1.6.41|>=2,<2.0.10|>=2.1,<2.1.1",
-                "swiftmailer/swiftmailer": ">=4,<5.4.5",
-                "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
-                "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
-                "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
-                "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
-                "sylius/sylius": "<1.6.9|>=1.7,<1.7.9|>=1.8,<1.8.3|>=1.9,<1.9.5",
-                "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
-                "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4",
-                "symbiote/silverstripe-versionedfiles": "<=2.0.3",
-                "symfont/process": ">=0,<4",
-                "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
-                "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
-                "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
-                "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
-                "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
-                "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
-                "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
-                "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
-                "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1",
-                "symfony/mime": ">=4.3,<4.3.8",
-                "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
-                "symfony/polyfill": ">=1,<1.10",
-                "symfony/polyfill-php55": ">=1,<1.10",
-                "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
-                "symfony/routing": ">=2,<2.0.19",
-                "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8",
-                "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
-                "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9",
-                "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
-                "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
-                "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8|>=5.3,<5.3.2",
-                "symfony/serializer": ">=2,<2.0.11",
-                "symfony/symfony": ">=2,<3.4.49|>=4,<4.4.24|>=5,<5.2.9|>=5.3,<5.3.2",
-                "symfony/translation": ">=2,<2.0.17",
-                "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
-                "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
-                "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4",
-                "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7",
-                "t3/dce": ">=2.2,<2.6.2",
-                "t3g/svg-sanitizer": "<1.0.3",
-                "tecnickcom/tcpdf": "<6.2.22",
-                "thelia/backoffice-default-template": ">=2.1,<2.1.2",
-                "thelia/thelia": ">=2.1-beta.1,<2.1.3",
-                "theonedemon/phpwhois": "<=4.2.5",
-                "titon/framework": ">=0,<9.9.99",
-                "topthink/think": "<=6.0.9",
-                "topthink/thinkphp": "<=3.2.3",
-                "tribalsystems/zenario": "<8.8.53370",
-                "truckersmp/phpwhois": "<=4.3.1",
-                "twig/twig": "<1.38|>=2,<2.7",
-                "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.38|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
-                "typo3/cms-backend": ">=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
-                "typo3/cms-core": ">=6.2,<=6.2.56|>=7,<=7.6.52|>=8,<=8.7.41|>=9,<9.5.29|>=10,<10.4.19|>=11,<11.3.2",
-                "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
-                "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
-                "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
-                "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
-                "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5",
-                "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10",
-                "ua-parser/uap-php": "<3.8",
-                "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
-                "vanilla/safecurl": "<0.9.2",
-                "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
-                "vrana/adminer": "<4.7.9",
-                "wallabag/tcpdf": "<6.2.22",
-                "web-auth/webauthn-framework": ">=3.3,<3.3.4",
-                "webcoast/deferred-image-processing": "<1.0.2",
-                "wikimedia/parsoid": "<0.12.2",
-                "willdurand/js-translation-bundle": "<2.1.1",
-                "wp-cli/wp-cli": "<2.5",
-                "yidashi/yii2cmf": "<=2",
-                "yii2mod/yii2-cms": "<1.9.2",
-                "yiisoft/yii": ">=1.1.14,<1.1.15",
-                "yiisoft/yii2": "<2.0.38",
-                "yiisoft/yii2-bootstrap": "<2.0.4",
-                "yiisoft/yii2-dev": "<2.0.43",
-                "yiisoft/yii2-elasticsearch": "<2.0.5",
-                "yiisoft/yii2-gii": "<2.0.4",
-                "yiisoft/yii2-jui": "<2.0.4",
-                "yiisoft/yii2-redis": "<2.0.8",
-                "yoast-seo-for-typo3/yoast_seo": "<7.2.3",
-                "yourls/yourls": "<=1.8.2",
-                "zendesk/zendesk_api_client_php": "<2.2.11",
-                "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
-                "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
-                "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
-                "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5",
-                "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3",
-                "zendframework/zend-diactoros": ">=1,<1.8.4",
-                "zendframework/zend-feed": ">=1,<2.10.3",
-                "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1",
-                "zendframework/zend-http": ">=1,<2.8.1",
-                "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6",
-                "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3",
-                "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2",
-                "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1",
-                "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4",
-                "zendframework/zend-validator": ">=2.3,<2.3.6",
-                "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1",
-                "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6",
-                "zendframework/zendframework": "<=3",
-                "zendframework/zendframework1": "<1.12.20",
-                "zendframework/zendopenid": ">=2,<2.0.2",
-                "zendframework/zendxml": ">=1,<1.0.1",
-                "zetacomponents/mail": "<1.8.2",
-                "zf-commons/zfc-user": "<1.2.2",
-                "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
-                "zfr/zfr-oauth2-server-module": "<0.1.2",
-                "zoujingli/thinkadmin": "<6.0.22"
-            },
-            "default-branch": true,
-            "type": "metapackage",
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Marco Pivetta",
-                    "email": "ocramius@gmail.com",
-                    "role": "maintainer"
-                },
-                {
-                    "name": "Ilya Tribusean",
-                    "email": "slash3b@gmail.com",
-                    "role": "maintainer"
-                }
-            ],
-            "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
-            "support": {
-                "issues": "https://github.com/Roave/SecurityAdvisories/issues",
-                "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/Ocramius",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-09-30T18:03:50+00:00"
-        },
-        {
-            "name": "rskuipers/php-assumptions",
-            "version": "0.8.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/rskuipers/php-assumptions.git",
-                "reference": "10f82ca2a268d6624553d2de4659ed6451d9ac60"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/rskuipers/php-assumptions/zipball/10f82ca2a268d6624553d2de4659ed6451d9ac60",
-                "reference": "10f82ca2a268d6624553d2de4659ed6451d9ac60",
-                "shasum": ""
-            },
-            "require": {
-                "league/climate": "^3.1",
-                "nikic/php-parser": "^2.0|^3.0|^4.0"
-            },
-            "require-dev": {
-                "phpspec/prophecy": "~1.0",
-                "phpunit/phpunit": "^5.6"
-            },
-            "bin": [
-                "bin/phpa"
-            ],
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "PhpAssumptions\\": "src/PhpAssumptions"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Rick Kuipers",
-                    "email": "io@rskuipers.com"
-                }
-            ],
-            "description": "Static code analysis tool to detect weak assumptions",
-            "support": {
-                "issues": "https://github.com/rskuipers/php-assumptions/issues",
-                "source": "https://github.com/rskuipers/php-assumptions/tree/0.8.0"
-            },
-            "time": "2019-04-04T07:11:49+00:00"
-        },
-        {
-            "name": "sebastian/code-unit-reverse-lookup",
-            "version": "1.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
-                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
-                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.6"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.5"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Looks up which function or method a line of code belongs to",
-            "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
-                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T08:15:22+00:00"
-        },
-        {
-            "name": "sebastian/comparator",
-            "version": "3.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
-                "reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1",
-                "sebastian/diff": "^3.0",
-                "sebastian/exporter": "^3.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.5"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Volker Dusch",
-                    "email": "github@wallbash.com"
-                },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@2bepublished.at"
-                }
-            ],
-            "description": "Provides the functionality to compare PHP values for equality",
-            "homepage": "https://github.com/sebastianbergmann/comparator",
-            "keywords": [
-                "comparator",
-                "compare",
-                "equality"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/comparator/issues",
-                "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T08:04:30+00:00"
-        },
-        {
-            "name": "sebastian/diff",
-            "version": "3.0.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
-                "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^7.5 || ^8.0",
-                "symfony/process": "^2 || ^3.3 || ^4"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                }
-            ],
-            "description": "Diff implementation",
-            "homepage": "https://github.com/sebastianbergmann/diff",
-            "keywords": [
-                "diff",
-                "udiff",
-                "unidiff",
-                "unified diff"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/diff/issues",
-                "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:59:04+00:00"
-        },
-        {
-            "name": "sebastian/environment",
-            "version": "4.2.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
-                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^7.5"
-            },
-            "suggest": {
-                "ext-posix": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.2-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Provides functionality to handle HHVM/PHP environments",
-            "homepage": "http://www.github.com/sebastianbergmann/environment",
-            "keywords": [
-                "Xdebug",
-                "environment",
-                "hhvm"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/environment/issues",
-                "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:53:42+00:00"
-        },
-        {
-            "name": "sebastian/exporter",
-            "version": "3.1.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e",
-                "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.0",
-                "sebastian/recursion-context": "^3.0"
-            },
-            "require-dev": {
-                "ext-mbstring": "*",
-                "phpunit/phpunit": "^6.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Volker Dusch",
-                    "email": "github@wallbash.com"
-                },
-                {
-                    "name": "Adam Harvey",
-                    "email": "aharvey@php.net"
-                },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@gmail.com"
-                }
-            ],
-            "description": "Provides the functionality to export PHP variables for visualization",
-            "homepage": "http://www.github.com/sebastianbergmann/exporter",
-            "keywords": [
-                "export",
-                "exporter"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/exporter/issues",
-                "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:47:53+00:00"
-        },
-        {
-            "name": "sebastian/finder-facade",
-            "version": "1.2.3",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/finder-facade.git",
-                "reference": "167c45d131f7fc3d159f56f191a0a22228765e16"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16",
-                "reference": "167c45d131f7fc3d159f56f191a0a22228765e16",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.1",
-                "symfony/finder": "^2.3|^3.0|^4.0|^5.0",
-                "theseer/fdomdocument": "^1.6"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": []
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
-            "homepage": "https://github.com/sebastianbergmann/finder-facade",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/finder-facade/issues",
-                "source": "https://github.com/sebastianbergmann/finder-facade/tree/1.2"
-            },
-            "abandoned": true,
-            "time": "2020-01-16T08:08:45+00:00"
-        },
-        {
-            "name": "sebastian/global-state",
-            "version": "3.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b",
-                "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.2",
-                "sebastian/object-reflector": "^1.1.1",
-                "sebastian/recursion-context": "^3.0"
-            },
-            "require-dev": {
-                "ext-dom": "*",
-                "phpunit/phpunit": "^8.0"
-            },
-            "suggest": {
-                "ext-uopz": "*"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Snapshotting of global state",
-            "homepage": "http://www.github.com/sebastianbergmann/global-state",
-            "keywords": [
-                "global state"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/global-state/issues",
-                "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:43:24+00:00"
-        },
-        {
-            "name": "sebastian/object-enumerator",
-            "version": "3.0.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/object-enumerator.git",
-                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
-                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.0",
-                "sebastian/object-reflector": "^1.1.1",
-                "sebastian/recursion-context": "^3.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^6.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Traverses array structures and object graphs to enumerate all referenced objects",
-            "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
-                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:40:27+00:00"
-        },
-        {
-            "name": "sebastian/object-reflector",
-            "version": "1.1.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/object-reflector.git",
-                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
-                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^6.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Allows reflection of object attributes, including inherited and non-public ones",
-            "homepage": "https://github.com/sebastianbergmann/object-reflector/",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
-                "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:37:18+00:00"
-        },
-        {
-            "name": "sebastian/phpcpd",
-            "version": "4.1.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpcpd.git",
-                "reference": "0d9afa762f2400de077b2192f4a9d127de0bb78e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/0d9afa762f2400de077b2192f4a9d127de0bb78e",
-                "reference": "0d9afa762f2400de077b2192f4a9d127de0bb78e",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "php": "^7.1",
-                "phpunit/php-timer": "^2.0",
-                "sebastian/finder-facade": "^1.1",
-                "sebastian/version": "^1.0|^2.0",
-                "symfony/console": "^2.7|^3.0|^4.0"
-            },
-            "bin": [
-                "phpcpd"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Copy/Paste Detector (CPD) for PHP code.",
-            "homepage": "https://github.com/sebastianbergmann/phpcpd",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/phpcpd/issues",
-                "source": "https://github.com/sebastianbergmann/phpcpd/tree/4.1.0"
-            },
-            "time": "2018-09-17T17:17:27+00:00"
-        },
-        {
-            "name": "sebastian/recursion-context",
-            "version": "3.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
-                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^6.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Jeff Welch",
-                    "email": "whatthejeff@gmail.com"
-                },
-                {
-                    "name": "Adam Harvey",
-                    "email": "aharvey@php.net"
-                }
-            ],
-            "description": "Provides functionality to recursively process PHP variables",
-            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
-                "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:34:24+00:00"
-        },
-        {
-            "name": "sebastian/resource-operations",
-            "version": "2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/resource-operations.git",
-                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
-                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                }
-            ],
-            "description": "Provides a list of PHP built-in functions that operate on resources",
-            "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
-                "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "abandoned": true,
-            "time": "2020-11-30T07:30:19+00:00"
-        },
-        {
-            "name": "sebastian/type",
-            "version": "1.1.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/type.git",
-                "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4",
-                "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.2"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Collection of value objects that represent the types of the PHP type system",
-            "homepage": "https://github.com/sebastianbergmann/type",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/type/issues",
-                "source": "https://github.com/sebastianbergmann/type/tree/1.1.4"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-11-30T07:25:11+00:00"
-        },
-        {
-            "name": "sebastian/version",
-            "version": "2.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/version.git",
-                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
-                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.6"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.0.x-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
-            "homepage": "https://github.com/sebastianbergmann/version",
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/version/issues",
-                "source": "https://github.com/sebastianbergmann/version/tree/master"
-            },
-            "time": "2016-10-03T07:35:21+00:00"
-        },
-        {
-            "name": "seld/cli-prompt",
-            "version": "1.0.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/Seldaek/cli-prompt.git",
-                "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5",
-                "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3"
-            },
-            "require-dev": {
-                "phpstan/phpstan": "^0.12.63"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Seld\\CliPrompt\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Jordi Boggiano",
-                    "email": "j.boggiano@seld.be"
-                }
-            ],
-            "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type",
-            "keywords": [
-                "cli",
-                "console",
-                "hidden",
-                "input",
-                "prompt"
-            ],
-            "support": {
-                "issues": "https://github.com/Seldaek/cli-prompt/issues",
-                "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4"
-            },
-            "time": "2020-12-15T21:32:01+00:00"
-        },
-        {
-            "name": "symfony/config",
-            "version": "v4.4.30",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/config.git",
-                "reference": "d9ea72de055cd822e5228ff898e2aad2f52f76b0"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/d9ea72de055cd822e5228ff898e2aad2f52f76b0",
-                "reference": "d9ea72de055cd822e5228ff898e2aad2f52f76b0",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/filesystem": "^3.4|^4.0|^5.0",
-                "symfony/polyfill-ctype": "~1.8",
-                "symfony/polyfill-php80": "^1.16",
-                "symfony/polyfill-php81": "^1.22"
-            },
-            "conflict": {
-                "symfony/finder": "<3.4"
-            },
-            "require-dev": {
-                "symfony/event-dispatcher": "^3.4|^4.0|^5.0",
-                "symfony/finder": "^3.4|^4.0|^5.0",
-                "symfony/messenger": "^4.1|^5.0",
-                "symfony/service-contracts": "^1.1|^2",
-                "symfony/yaml": "^3.4|^4.0|^5.0"
-            },
-            "suggest": {
-                "symfony/yaml": "To use the yaml reference dumper"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Config\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/config/tree/v4.4.30"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-08-04T20:31:23+00:00"
-        },
-        {
-            "name": "symfony/console",
-            "version": "v4.4.30",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/console.git",
-                "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22",
-                "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/polyfill-mbstring": "~1.0",
-                "symfony/polyfill-php73": "^1.8",
-                "symfony/polyfill-php80": "^1.16",
-                "symfony/service-contracts": "^1.1|^2"
-            },
-            "conflict": {
-                "psr/log": ">=3",
-                "symfony/dependency-injection": "<3.4",
-                "symfony/event-dispatcher": "<4.3|>=5",
-                "symfony/lock": "<4.4",
-                "symfony/process": "<3.3"
-            },
-            "provide": {
-                "psr/log-implementation": "1.0|2.0"
-            },
-            "require-dev": {
-                "psr/log": "^1|^2",
-                "symfony/config": "^3.4|^4.0|^5.0",
-                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
-                "symfony/event-dispatcher": "^4.3",
-                "symfony/lock": "^4.4|^5.0",
-                "symfony/process": "^3.4|^4.0|^5.0",
-                "symfony/var-dumper": "^4.3|^5.0"
-            },
-            "suggest": {
-                "psr/log": "For using the console logger",
-                "symfony/event-dispatcher": "",
-                "symfony/lock": "",
-                "symfony/process": ""
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Console\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Eases the creation of beautiful and testable command line interfaces",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/console/tree/v4.4.30"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-08-25T19:27:26+00:00"
-        },
-        {
-            "name": "symfony/dependency-injection",
-            "version": "v4.4.31",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "75dd7094870feaa5be9ed2b6b1efcfc2b7d3b9b4"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/75dd7094870feaa5be9ed2b6b1efcfc2b7d3b9b4",
-                "reference": "75dd7094870feaa5be9ed2b6b1efcfc2b7d3b9b4",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "psr/container": "^1.0",
-                "symfony/polyfill-php80": "^1.16",
-                "symfony/service-contracts": "^1.1.6|^2"
-            },
-            "conflict": {
-                "symfony/config": "<4.3|>=5.0",
-                "symfony/finder": "<3.4",
-                "symfony/proxy-manager-bridge": "<3.4",
-                "symfony/yaml": "<3.4"
-            },
-            "provide": {
-                "psr/container-implementation": "1.0",
-                "symfony/service-implementation": "1.0|2.0"
-            },
-            "require-dev": {
-                "symfony/config": "^4.3",
-                "symfony/expression-language": "^3.4|^4.0|^5.0",
-                "symfony/yaml": "^4.4|^5.0"
-            },
-            "suggest": {
-                "symfony/config": "",
-                "symfony/expression-language": "For using expressions in service container configuration",
-                "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required",
-                "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them",
-                "symfony/yaml": ""
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\DependencyInjection\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Allows you to standardize and centralize the way objects are constructed in your application",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/dependency-injection/tree/v4.4.31"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-09-21T06:20:06+00:00"
-        },
-        {
-            "name": "symfony/event-dispatcher",
-            "version": "v4.4.30",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "2fe81680070043c4c80e7cedceb797e34f377bac"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac",
-                "reference": "2fe81680070043c4c80e7cedceb797e34f377bac",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/event-dispatcher-contracts": "^1.1",
-                "symfony/polyfill-php80": "^1.16"
-            },
-            "conflict": {
-                "symfony/dependency-injection": "<3.4"
-            },
-            "provide": {
-                "psr/event-dispatcher-implementation": "1.0",
-                "symfony/event-dispatcher-implementation": "1.1"
-            },
-            "require-dev": {
-                "psr/log": "^1|^2|^3",
-                "symfony/config": "^3.4|^4.0|^5.0",
-                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
-                "symfony/error-handler": "~3.4|~4.4",
-                "symfony/expression-language": "^3.4|^4.0|^5.0",
-                "symfony/http-foundation": "^3.4|^4.0|^5.0",
-                "symfony/service-contracts": "^1.1|^2",
-                "symfony/stopwatch": "^3.4|^4.0|^5.0"
-            },
-            "suggest": {
-                "symfony/dependency-injection": "",
-                "symfony/http-kernel": ""
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\EventDispatcher\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-08-04T20:31:23+00:00"
-        },
-        {
-            "name": "symfony/event-dispatcher-contracts",
-            "version": "v1.1.9",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
-                "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7",
-                "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3"
-            },
-            "suggest": {
-                "psr/event-dispatcher": "",
-                "symfony/event-dispatcher-implementation": ""
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1-dev"
-                },
-                "thanks": {
-                    "name": "symfony/contracts",
-                    "url": "https://github.com/symfony/contracts"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Contracts\\EventDispatcher\\": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Generic abstractions related to dispatching event",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "abstractions",
-                "contracts",
-                "decoupling",
-                "interfaces",
-                "interoperability",
-                "standards"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-07-06T13:19:58+00:00"
-        },
-        {
-            "name": "symfony/filesystem",
-            "version": "v4.4.27",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/filesystem.git",
-                "reference": "517fb795794faf29086a77d99eb8f35e457837a7"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/517fb795794faf29086a77d99eb8f35e457837a7",
-                "reference": "517fb795794faf29086a77d99eb8f35e457837a7",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/polyfill-ctype": "~1.8",
-                "symfony/polyfill-php80": "^1.16"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Filesystem\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Provides basic utilities for the filesystem",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/filesystem/tree/v4.4.27"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-21T12:19:41+00:00"
-        },
-        {
-            "name": "symfony/finder",
-            "version": "v4.4.30",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/finder.git",
-                "reference": "70362f1e112280d75b30087c7598b837c1b468b6"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/70362f1e112280d75b30087c7598b837c1b468b6",
-                "reference": "70362f1e112280d75b30087c7598b837c1b468b6",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/polyfill-php80": "^1.16"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Finder\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Finds files and directories via an intuitive fluent interface",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/finder/tree/v4.4.30"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-08-04T20:31:23+00:00"
-        },
-        {
-            "name": "symfony/options-resolver",
-            "version": "v4.4.30",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/options-resolver.git",
-                "reference": "fa0b12a3a47ed25749d47d6b4f61412fd5ca1554"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/fa0b12a3a47ed25749d47d6b4f61412fd5ca1554",
-                "reference": "fa0b12a3a47ed25749d47d6b4f61412fd5ca1554",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/polyfill-php80": "^1.16"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\OptionsResolver\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Provides an improved replacement for the array_replace PHP function",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "config",
-                "configuration",
-                "options"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/options-resolver/tree/v4.4.30"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-08-04T20:31:23+00:00"
-        },
-        {
-            "name": "symfony/polyfill-ctype",
-            "version": "v1.23.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
-                "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "suggest": {
-                "ext-ctype": "For best performance"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Ctype\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Gert de Pagter",
-                    "email": "BackEndTea@gmail.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill for ctype functions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "ctype",
-                "polyfill",
-                "portable"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-02-19T12:13:01+00:00"
-        },
-        {
-            "name": "symfony/polyfill-intl-idn",
-            "version": "v1.23.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65",
-                "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1",
-                "symfony/polyfill-intl-normalizer": "^1.10",
-                "symfony/polyfill-php72": "^1.10"
-            },
-            "suggest": {
-                "ext-intl": "For best performance"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Laurent Bassin",
-                    "email": "laurent@bassin.info"
-                },
-                {
-                    "name": "Trevor Rowbotham",
-                    "email": "trevor.rowbotham@pm.me"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "idn",
-                "intl",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-05-27T09:27:20+00:00"
-        },
-        {
-            "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.23.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
-                "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "suggest": {
-                "ext-intl": "For best performance"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ],
-                "classmap": [
-                    "Resources/stubs"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill for intl's Normalizer class and related functions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "intl",
-                "normalizer",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-02-19T12:13:01+00:00"
-        },
-        {
-            "name": "symfony/polyfill-php72",
-            "version": "v1.23.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
-                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php72\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-05-27T09:17:38+00:00"
-        },
-        {
-            "name": "symfony/polyfill-php73",
-            "version": "v1.23.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php73.git",
-                "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010",
-                "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php73\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ],
-                "classmap": [
-                    "Resources/stubs"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-02-19T12:13:01+00:00"
-        },
-        {
-            "name": "symfony/polyfill-php80",
-            "version": "v1.23.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
-                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php80\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ],
-                "classmap": [
-                    "Resources/stubs"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Ion Bazan",
-                    "email": "ion.bazan@gmail.com"
-                },
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-28T13:41:28+00:00"
-        },
-        {
-            "name": "symfony/polyfill-php81",
-            "version": "v1.23.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php81.git",
-                "reference": "e66119f3de95efc359483f810c4c3e6436279436"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436",
-                "reference": "e66119f3de95efc359483f810c4c3e6436279436",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.23-dev"
-                },
-                "thanks": {
-                    "name": "symfony/polyfill",
-                    "url": "https://github.com/symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php81\\": ""
-                },
-                "files": [
-                    "bootstrap.php"
-                ],
-                "classmap": [
-                    "Resources/stubs"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-05-21T13:25:03+00:00"
-        },
-        {
-            "name": "symfony/process",
-            "version": "v4.4.30",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/process.git",
-                "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
-                "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/polyfill-php80": "^1.16"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Process\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Executes commands in sub-processes",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/process/tree/v4.4.30"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-08-04T20:31:23+00:00"
-        },
-        {
-            "name": "symfony/service-contracts",
-            "version": "v1.1.9",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/service-contracts.git",
-                "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b776d18b303a39f56c63747bcb977ad4b27aca26",
-                "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "psr/container": "^1.0"
-            },
-            "suggest": {
-                "symfony/service-implementation": ""
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.1-dev"
-                },
-                "thanks": {
-                    "name": "symfony/contracts",
-                    "url": "https://github.com/symfony/contracts"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Contracts\\Service\\": ""
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Generic abstractions related to writing services",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "abstractions",
-                "contracts",
-                "decoupling",
-                "interfaces",
-                "interoperability",
-                "standards"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/service-contracts/tree/v1.1.9"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-07-06T13:19:58+00:00"
-        },
-        {
-            "name": "symfony/stopwatch",
-            "version": "v4.4.27",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/stopwatch.git",
-                "reference": "c85d997af06a58ba83e2d2538e335b894c24523d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/stopwatch/zipball/c85d997af06a58ba83e2d2538e335b894c24523d",
-                "reference": "c85d997af06a58ba83e2d2538e335b894c24523d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/service-contracts": "^1.0|^2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Stopwatch\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Provides a way to profile code",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/stopwatch/tree/v4.4.27"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-10T08:41:57+00:00"
-        },
-        {
-            "name": "symfony/yaml",
-            "version": "v4.4.29",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/yaml.git",
-                "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/3abcc4db06d4e776825eaa3ed8ad924d5bc7432a",
-                "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.1.3",
-                "symfony/polyfill-ctype": "~1.8"
-            },
-            "conflict": {
-                "symfony/console": "<3.4"
-            },
-            "require-dev": {
-                "symfony/console": "^3.4|^4.0|^5.0"
-            },
-            "suggest": {
-                "symfony/console": "For validating YAML files using the lint command"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Yaml\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Loads and dumps YAML files",
-            "homepage": "https://symfony.com",
-            "support": {
-                "source": "https://github.com/symfony/yaml/tree/v4.4.29"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-07-27T16:19:30+00:00"
-        },
-        {
-            "name": "theseer/fdomdocument",
-            "version": "1.6.6",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/theseer/fDOMDocument.git",
-                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
-                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "lib-libxml": "*",
-                "php": ">=5.3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "lead"
-                }
-            ],
-            "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
-            "homepage": "https://github.com/theseer/fDOMDocument",
-            "support": {
-                "issues": "https://github.com/theseer/fDOMDocument/issues",
-                "source": "https://github.com/theseer/fDOMDocument/tree/master"
-            },
-            "time": "2017-06-30T11:53:12+00:00"
-        },
-        {
-            "name": "theseer/tokenizer",
-            "version": "1.2.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
-                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-tokenizer": "*",
-                "ext-xmlwriter": "*",
-                "php": "^7.2 || ^8.0"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
-                }
-            ],
-            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
-            "support": {
-                "issues": "https://github.com/theseer/tokenizer/issues",
-                "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/theseer",
-                    "type": "github"
-                }
-            ],
-            "time": "2021-07-28T10:34:58+00:00"
-        },
-        {
-            "name": "ukko/phpredis-phpdoc",
-            "version": "dev-master",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/ukko/phpredis-phpdoc.git",
-                "reference": "5ce7ea21e7240f4c60ab9a0f5a9208ea6c2a2f30"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/ukko/phpredis-phpdoc/zipball/5ce7ea21e7240f4c60ab9a0f5a9208ea6c2a2f30",
-                "reference": "5ce7ea21e7240f4c60ab9a0f5a9208ea6c2a2f30",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.0"
-            },
-            "default-branch": true,
-            "type": "library",
-            "autoload": {
-                "psr-0": {
-                    "phpredis-phpdoc": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Max Kamashev",
-                    "email": "max.kamashev@gmail.com",
-                    "homepage": "http://max.kamashev.name/",
-                    "role": "Developer"
-                }
-            ],
-            "description": "@phpdoc extension phpredis for IDE autocomplete",
-            "homepage": "https://github.com/ukko/phpredis-phpdoc",
-            "keywords": [
-                "helper",
-                "phpredis",
-                "redis"
-            ],
-            "support": {
-                "issues": "https://github.com/ukko/phpredis-phpdoc/issues",
-                "source": "https://github.com/ukko/phpredis-phpdoc/tree/master"
-            },
-            "time": "2021-05-08T16:57:08+00:00"
-        },
-        {
-            "name": "vimeo/psalm",
-            "version": "4.10.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/vimeo/psalm.git",
-                "reference": "916b098b008f6de4543892b1e0651c1c3b92cbfa"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/vimeo/psalm/zipball/916b098b008f6de4543892b1e0651c1c3b92cbfa",
-                "reference": "916b098b008f6de4543892b1e0651c1c3b92cbfa",
-                "shasum": ""
-            },
-            "require": {
-                "amphp/amp": "^2.4.2",
-                "amphp/byte-stream": "^1.5",
-                "composer/package-versions-deprecated": "^1.8.0",
-                "composer/semver": "^1.4 || ^2.0 || ^3.0",
-                "composer/xdebug-handler": "^1.1 || ^2.0",
-                "dnoegel/php-xdg-base-dir": "^0.1.1",
-                "ext-ctype": "*",
-                "ext-dom": "*",
-                "ext-json": "*",
-                "ext-libxml": "*",
-                "ext-mbstring": "*",
-                "ext-simplexml": "*",
-                "ext-tokenizer": "*",
-                "felixfbecker/advanced-json-rpc": "^3.0.3",
-                "felixfbecker/language-server-protocol": "^1.5",
-                "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
-                "nikic/php-parser": "^4.12",
-                "openlss/lib-array2xml": "^1.0",
-                "php": "^7.1|^8",
-                "sebastian/diff": "^3.0 || ^4.0",
-                "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0",
-                "webmozart/path-util": "^2.3"
-            },
-            "provide": {
-                "psalm/psalm": "self.version"
-            },
-            "require-dev": {
-                "bamarni/composer-bin-plugin": "^1.2",
-                "brianium/paratest": "^4.0||^6.0",
-                "ext-curl": "*",
-                "php-parallel-lint/php-parallel-lint": "^1.2",
-                "phpdocumentor/reflection-docblock": "^5",
-                "phpmyadmin/sql-parser": "5.1.0||dev-master",
-                "phpspec/prophecy": ">=1.9.0",
-                "phpunit/phpunit": "^9.0",
-                "psalm/plugin-phpunit": "^0.16",
-                "slevomat/coding-standard": "^7.0",
-                "squizlabs/php_codesniffer": "^3.5",
-                "symfony/process": "^4.3 || ^5.0",
-                "weirdan/prophecy-shim": "^1.0 || ^2.0"
-            },
-            "suggest": {
-                "ext-igbinary": "^2.0.5"
-            },
-            "bin": [
-                "psalm",
-                "psalm-language-server",
-                "psalm-plugin",
-                "psalm-refactor",
-                "psalter"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.x-dev",
-                    "dev-3.x": "3.x-dev",
-                    "dev-2.x": "2.x-dev",
-                    "dev-1.x": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psalm\\": "src/Psalm/"
-                },
-                "files": [
-                    "src/functions.php",
-                    "src/spl_object_id.php"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Matthew Brown"
-                }
-            ],
-            "description": "A static analysis tool for finding errors in PHP applications",
-            "keywords": [
-                "code",
-                "inspection",
-                "php"
-            ],
-            "support": {
-                "issues": "https://github.com/vimeo/psalm/issues",
-                "source": "https://github.com/vimeo/psalm/tree/4.10.0"
-            },
-            "time": "2021-09-04T21:00:09+00:00"
-        },
-        {
-            "name": "webmozart/assert",
-            "version": "1.10.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/webmozarts/assert.git",
-                "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
-                "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0",
-                "symfony/polyfill-ctype": "^1.8"
-            },
-            "conflict": {
-                "phpstan/phpstan": "<0.12.20",
-                "vimeo/psalm": "<4.6.1 || 4.6.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.5.13"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.10-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Webmozart\\Assert\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@gmail.com"
-                }
-            ],
-            "description": "Assertions to validate method input/output with nice error messages.",
-            "keywords": [
-                "assert",
-                "check",
-                "validate"
-            ],
-            "support": {
-                "issues": "https://github.com/webmozarts/assert/issues",
-                "source": "https://github.com/webmozarts/assert/tree/1.10.0"
-            },
-            "time": "2021-03-09T10:59:23+00:00"
-        },
-        {
-            "name": "webmozart/path-util",
-            "version": "2.3.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/webmozart/path-util.git",
-                "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
-                "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.3.3",
-                "webmozart/assert": "~1.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^4.6",
-                "sebastian/version": "^1.0.1"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.3-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Webmozart\\PathUtil\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "bschussek@gmail.com"
-                }
-            ],
-            "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.",
-            "support": {
-                "issues": "https://github.com/webmozart/path-util/issues",
-                "source": "https://github.com/webmozart/path-util/tree/2.3.0"
-            },
-            "time": "2015-12-17T08:42:14+00:00"
-        }
-    ],
-    "aliases": [],
-    "minimum-stability": "stable",
-    "stability-flags": {
-        "redisent/redisent": 20,
-        "roave/security-advisories": 20,
-        "ukko/phpredis-phpdoc": 20
-    },
-    "prefer-stable": false,
-    "prefer-lowest": false,
-    "platform": {
-        "php": "^7.2",
-        "ext-intl": "*"
-    },
-    "platform-dev": [],
-    "platform-overrides": {
-        "php": "7.2"
-    },
-    "plugin-api-version": "2.0.0"
-}

From fc7d92051f73c58000cc665f10afeb1b4695b5b5 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 9 Nov 2021 20:35:03 +0100
Subject: [PATCH 22/60] Fix AmpRedisClient pipeline + fix Doc

---
 CHANGELOG.md                        | 10 +++++++++-
 README.md                           |  6 +++---
 src/Redis/Client/AmpRedisClient.php | 21 +++++++++++----------
 3 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3fc265..6fec1a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.0.1]
+
+### Fixed
+
+- (doc) Fix GPS coordinate (should be longitude first)
+- `AmpRedisClient` pipeline not sequential
+
 ## [2.0.0]
 
 ### Added
@@ -124,7 +131,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 First version
 
 [Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...HEAD
-[1.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/2.0.0
+[2.0.1]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...2.0.1
+[2.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/2.0.0
 [Unreleased v1.x]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...1.x
 [1.4.0]: https://github.com/MacFJA/php-redisearch/compare/1.3.0...1.4.0
 [1.3.0]: https://github.com/MacFJA/php-redisearch/compare/1.2.0...1.3.0
diff --git a/README.md b/README.md
index ce03001..a115603 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ $index->addDocumentFromArray([
     'firstname' => 'Joe',
     'lastname' => 'Doe',
     'age' => 30,
-    'address' => '40.689247,-74.044502'
+    'address' => '-74.044502,40.689247'
 ]);
 ```
 
@@ -130,14 +130,14 @@ $query = $queryBuilder
     ->addString('Doe')
     ->addElement(
         new Negation(
-            new GeoFacet(['address'], 40.589247, -74.044502, 40, GeoFilterOption::UNIT_KILOMETERS)
+            new GeoFacet(['address'], -74.044502, 40.589247, 40, GeoFilterOption::UNIT_KILOMETERS)
         )
     )
     ->addElement(new Optional(new Word('John')))
     ->render();
 
 // The value of $query is:
-// @age:[(17 +inf] Doe -@address:[40.589247 -74.044502 40.000000 km] ~John
+// @age:[(17 +inf] Doe -@address:[-74.044502 40.589247 40.000000 km] ~John
 ```
 
 ### Pipeline
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
index 3a83d1d..659fbce 100644
--- a/src/Redis/Client/AmpRedisClient.php
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -21,7 +21,6 @@
 
 namespace MacFJA\RediSearch\Redis\Client;
 
-use function Amp\Promise\all;
 use function Amp\Promise\wait;
 use Amp\Redis\Redis;
 use function function_exists;
@@ -39,7 +38,7 @@ private function __construct(Redis $redis)
         if (!static::supports($redis)) {
             throw new RuntimeException($this->getMissingMessage('Amp\Redis', false, [
                 Redis::class => ['query'],
-            ], ['\\Amp\\Promise\\wait', '\\Amp\\Promise\\all']));
+            ], ['\\Amp\\Promise\\wait']));
         }
         $this->redis = $redis;
     }
@@ -65,19 +64,21 @@ public static function supports($redis): bool
     {
         return $redis instanceof Redis
             && method_exists($redis, 'query')
-            && function_exists('\\Amp\\Promise\\wait')
-            && function_exists('\\Amp\\Promise\\all');
+            && function_exists('\\Amp\\Promise\\wait');
     }
 
-    protected function doPipeline(Command ...$commands): array
+    public function pipeline(Command ...$commands): array
     {
         false === static::$disableNotice
             && trigger_error('Warning, \\Amp\\Redis\\Redis don\'t use a real Redis Pipeline', E_USER_NOTICE);
 
-        return wait(all(
-            array_map(function (Command $command) {
-                return $this->redis->query($command->getId(), ...array_map('strval', $command->getArguments()));
-            }, $commands)
-        ));
+        return array_map(function (Command $command) {
+            return $this->execute($command);
+        }, $commands);
+    }
+
+    protected function doPipeline(Command ...$commands): array
+    {
+        return [];
     }
 }

From 963d99ceb259709786b7c7abbb45243ae49155e6 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Wed, 10 Nov 2021 22:10:05 +0100
Subject: [PATCH 23/60] Add reset method for builders

---
 CHANGELOG.md          |  4 ++++
 src/IndexBuilder.php  | 26 ++++++++++++++++++++++++++
 src/Query/Builder.php | 15 ++++++++++++++-
 3 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6fec1a1..7bc48a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Added
+
+- `reset` method for builder (`\MacFJA\RediSearch\IndexBuilder` and `\MacFJA\RediSearch\Query\Builder`)
+
 ## [2.0.1]
 
 ### Fixed
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 90a313f..6a1dfe5 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -147,6 +147,30 @@ public function __call(string $name, array $arguments): self
         throw new BadMethodCallException(sprintf('Call undefined %s method', $name));
     }
 
+    public function reset(): self
+    {
+        $this->index = null;
+        $this->structure = null;
+        $this->prefixes = [];
+        $this->filter = null;
+        $this->defaultLanguage = null;
+        $this->languageField = null;
+        $this->defaultScore = null;
+        $this->scoreField = null;
+        $this->payloadField = null;
+        $this->maxTextFields = false;
+        $this->temporary = null;
+        $this->noOffsets = false;
+        $this->noHighLight = false;
+        $this->noFields = false;
+        $this->noFrequencies = false;
+        $this->skipInitialScan = false;
+        $this->stopWords = null;
+        $this->fields = [];
+
+        return $this;
+    }
+
     public function setNoStopWords(): self
     {
         $this->stopWords = [];
@@ -168,6 +192,8 @@ public function create(Client $client, string $rediSearchVersion = AbstractComma
     {
         $command = $this->getCommand($rediSearchVersion);
 
+        $this->reset();
+
         return $client->execute($command);
     }
 
diff --git a/src/Query/Builder.php b/src/Query/Builder.php
index 36c9a26..2f801fb 100644
--- a/src/Query/Builder.php
+++ b/src/Query/Builder.php
@@ -30,14 +30,24 @@
 use MacFJA\RediSearch\Query\Builder\TagFacet;
 use MacFJA\RediSearch\Query\Builder\TextFacet;
 
+/**
+ * @SuppressWarnings(PHPMD.TooManyPublicMethods)
+ */
 class Builder implements QueryElement, QueryElementGroup
 {
     /** @var AndGroup */
     private $group;
 
     public function __construct()
+    {
+        $this->reset();
+    }
+
+    public function reset(): self
     {
         $this->group = new AndGroup();
+
+        return $this;
     }
 
     public function addTextFacet(string $field, string ...$values): self
@@ -97,7 +107,10 @@ public function addElement(QueryElement $element): QueryElementGroup
 
     public function render(?callable $escaper = null): string
     {
-        return $this->group->render(null);
+        $rendered = $this->group->render(null);
+        $this->reset();
+
+        return $rendered;
     }
 
     public function priority(): int

From 2960011007ab7b0b2f3140c190efbc6042b8544b Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Wed, 17 Nov 2021 22:14:52 +0100
Subject: [PATCH 24/60] Allow PHP 8

---
 CHANGELOG.md  | 1 +
 composer.json | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bc48a7..43d6fb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - `reset` method for builder (`\MacFJA\RediSearch\IndexBuilder` and `\MacFJA\RediSearch\Query\Builder`)
+- Allow PHP 8.0
 
 ## [2.0.1]
 
diff --git a/composer.json b/composer.json
index 0df4050..f994e70 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,7 @@
         }
     ],
     "require": {
-        "php": "^7.2",
+        "php": "^7.2 || ^8.0",
         "ext-intl": "*",
         "composer/semver": "^3.2",
         "respect/validation": "^2.0"

From ef75da949946636995ae74e8df7607a9f9e7e477 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Fri, 26 Nov 2021 20:28:57 +0100
Subject: [PATCH 25/60] Fix APPLY option of Aggregate command

---
 CHANGELOG.md                                  |  9 +++++
 composer.json                                 |  9 ++---
 src/Redis/Command/AbstractCommand.php         | 11 ++++++
 src/Redis/Command/Aggregate.php               | 33 +++++++++++++++++
 .../Command/AggregateCommand/ApplyOption.php  | 14 ++++++++
 tests/Redis/Command/AggregateTest.php         | 36 ++++++++++++++++++-
 tests/integration/DockerTest.php              | 24 ++++++++-----
 7 files changed, 120 insertions(+), 16 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43d6fb5..b01c64e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - `reset` method for builder (`\MacFJA\RediSearch\IndexBuilder` and `\MacFJA\RediSearch\Query\Builder`)
 - Allow PHP 8.0
+- (dev) Allow `podman` (in addition to `docker`) as container runner for integration test
+
+### Changed
+
+- `APPLY` option of Aggregate command is now dependent of the last inserted option (if the option have a meaning for `APPLY`)
+
+### Fixed
+
+- Fix the `APPLY` option of the Aggregate command
 
 ## [2.0.1]
 
diff --git a/composer.json b/composer.json
index f994e70..177edef 100644
--- a/composer.json
+++ b/composer.json
@@ -33,13 +33,13 @@
         "php-parallel-lint/php-parallel-lint": "^1.3",
         "phpmd/phpmd": "^2.10",
         "phpstan/phpstan": "^0.12.92",
-        "phpunit/phpunit": "^8.5",
+        "phpunit/phpunit": "^8.5 || ^9.3",
         "predis/predis": "^1.1",
         "ptrofimov/tinyredisclient": "^1.1",
         "redisent/redisent": "dev-master",
         "roave/security-advisories": "dev-latest",
         "rskuipers/php-assumptions": "^0.8.0",
-        "sebastian/phpcpd": "^4.1",
+        "sebastian/phpcpd": "^4.1 || ^6.0",
         "ukko/phpredis-phpdoc": "dev-master",
         "vimeo/psalm": "^4.7"
     },
@@ -54,11 +54,6 @@
         "ptrofimov/tinyredisclient": "To use TinyRedisClient implementation",
         "redisent/redisent": "To use Redisent implementation"
     },
-    "config": {
-        "platform": {
-            "php": "7.2"
-        }
-    },
     "autoload": {
         "psr-4": {
             "MacFJA\\RediSearch\\": "src/"
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index 08d8dc9..e9cedd5 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -82,6 +82,7 @@ public function getArguments(): array
         $arguments = array_filter($options, function (CommandOption $option) {
             return $option->isCompatible($this->rediSearchVersion) && $option->isValid();
         });
+        $arguments = $this->sortArguments($arguments);
 
         return array_reduce($arguments, function ($carry, CommandOption $option) {
             return array_merge($carry, $option->render($this->rediSearchVersion));
@@ -100,6 +101,16 @@ public function parseResponse($data)
         return $data;
     }
 
+    /**
+     * @param array<CommandOption> $arguments
+     *
+     * @return array<CommandOption>
+     */
+    protected function sortArguments(array $arguments): array
+    {
+        return $arguments;
+    }
+
     /**
      * @return array<string>
      */
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 94e3432..0cf110d 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function assert;
+use function count;
 use function is_array;
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption;
@@ -29,6 +30,7 @@
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\LimitOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor;
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
@@ -45,6 +47,9 @@ class Aggregate extends AbstractCommand implements PaginatedCommand
 {
     use ArrayResponseTrait;
 
+    /** @var CommandOption */
+    private $lastAdded;
+
     public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
     {
         parent::__construct([
@@ -64,6 +69,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
     public function setIndex(string $index): self
     {
         $this->options['index']->setValue($index);
+        $this->lastAdded = $this->options['index'];
 
         return $this;
     }
@@ -76,6 +82,7 @@ public function getIndex(): string
     public function setQuery(string $query): self
     {
         $this->options['query']->setValue($query);
+        $this->lastAdded = $this->options['query'];
 
         return $this;
     }
@@ -83,6 +90,7 @@ public function setQuery(string $query): self
     public function setVerbatim(bool $active = true): self
     {
         $this->options['verbatim']->setActive($active);
+        $this->lastAdded = $this->options['verbatim'];
 
         return $this;
     }
@@ -125,6 +133,8 @@ public function addApply(string $expression, string $alias): self
             ->setDataOfOption('expression', $expression)
             ->setDataOfOption('alias', $alias)
         ;
+        $apply->setParent($this->lastAdded);
+        $this->lastAdded = $apply;
         $this->options['apply'][] = $apply;
 
         return $this;
@@ -133,6 +143,7 @@ public function addApply(string $expression, string $alias): self
     public function addGroupBy(GroupByOption $group): self
     {
         $this->options['groupby'][] = $group;
+        $this->lastAdded = $group;
 
         return $this;
     }
@@ -140,6 +151,7 @@ public function addGroupBy(GroupByOption $group): self
     public function setLoad(string ...$field): self
     {
         $this->options['load']->setArguments($field);
+        $this->lastAdded = $this->options['load'];
 
         return $this;
     }
@@ -215,6 +227,27 @@ public function parseResponse($data)
         return new PaginatedResponse($this, $totalCount, $items);
     }
 
+    protected function sortArguments(array $arguments): array
+    {
+        /** @var array<ApplyOption> $applies */
+        $applies = array_filter($arguments, static function (CommandOption $option) {
+            return $option instanceof ApplyOption;
+        });
+        $withoutApplies = array_values(array_filter($arguments, static function (CommandOption $option) {
+            return !($option instanceof ApplyOption);
+        }));
+        for ($index = count($withoutApplies); $index > 0; --$index) {
+            $item = $withoutApplies[$index - 1];
+            foreach ($applies as $apply) {
+                if ($apply->getParent() === $item || (null === $apply->getParent() && 1 === $index)) {
+                    array_splice($withoutApplies, $index, 0, [$apply]);
+                }
+            }
+        }
+
+        return $withoutApplies;
+    }
+
     protected function getRequiredOptions(): array
     {
         return ['index', 'query'];
diff --git a/src/Redis/Command/AggregateCommand/ApplyOption.php b/src/Redis/Command/AggregateCommand/ApplyOption.php
index 384a2f0..b60a1a4 100644
--- a/src/Redis/Command/AggregateCommand/ApplyOption.php
+++ b/src/Redis/Command/AggregateCommand/ApplyOption.php
@@ -21,11 +21,15 @@
 
 namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
 
+use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 
 class ApplyOption extends GroupedOption
 {
+    /** @var null|CommandOption */
+    private $parent;
+
     public function __construct()
     {
         parent::__construct([
@@ -33,4 +37,14 @@ public function __construct()
             'alias' => new NamedOption('AS', null, '>=2.0.0'),
         ], ['expression'], [], '>=2.0.0');
     }
+
+    public function setParent(?CommandOption $parent): void
+    {
+        $this->parent = $parent;
+    }
+
+    public function getParent(): ?CommandOption
+    {
+        return $this->parent;
+    }
 }
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index d3873d7..23a93ae 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -138,8 +138,42 @@ public function testFullOption(): void
             'VERBATIM',
             'LOAD', 2, '@geo1', '@tag1',
             'GROUPBY', 1, '@text1', 'REDUCE', 'COUNT_DISTINCT', 1, '@user_id', 'REDUCE', 'AVG', 1, '@num1', 'AS', 'average',
-            'SORTBY', 3, '@text1', '@num1', 'DESC', 'MAX', 10,
             'APPLY', '@timestamp - (@timestamp % 86400)',  'AS', 'day',
+            'SORTBY', 3, '@text1', '@num1', 'DESC', 'MAX', 10,
+            'LIMIT', 12, 35,
+            'FILTER', "@name=='foo' && @age < 20",
+            'WITHCURSOR', 'COUNT', 20, 'MAXIDLE', 30,
+        ], $command->getArguments());
+    }
+
+    public function testApplyOrder(): void
+    {
+        $command = new Aggregate();
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setVerbatim()
+            ->setLoad('@geo1', '@tag1')
+            ->addApply('split(@tag1)', 'tag2')
+            ->addGroupBy(new GroupByOption(['@text1'], [ReduceOption::countDistinct('user_id'), ReduceOption::average('num1', 'average')]))
+            ->addFilter("@name=='foo' && @age < 20")
+            ->addApply('@age - 1', 'age')
+            ->addSortBy('@text1')
+            ->addSortBy('@num1', 'DESC')
+            ->setSortByMax(10)
+            ->setLimit(12, 35)
+            ->setWithCursor(20, 30)
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'VERBATIM',
+            'LOAD', 2, '@geo1', '@tag1',
+            'APPLY', 'split(@tag1)',  'AS', 'tag2',
+            'GROUPBY', 1, '@text1', 'REDUCE', 'COUNT_DISTINCT', 1, '@user_id', 'REDUCE', 'AVG', 1, '@num1', 'AS', 'average',
+            'APPLY', '@age - 1',  'AS', 'age',
+            'SORTBY', 3, '@text1', '@num1', 'DESC', 'MAX', 10,
             'LIMIT', 12, 35,
             'FILTER', "@name=='foo' && @age < 20",
             'WITHCURSOR', 'COUNT', 20, 'MAXIDLE', 30,
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 22ec2a2..ad6686a 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -26,6 +26,7 @@
 use Closure;
 use Credis_Client;
 use function get_class;
+use function is_string;
 use MacFJA\RediSearch\Index;
 use MacFJA\RediSearch\IndexBuilder;
 use MacFJA\RediSearch\Query\Builder;
@@ -110,30 +111,37 @@ class DockerTest extends TestCase
 {
     /** @var bool */
     private static $skip = false;
+    /** @var string */
+    private static $containerCommand = 'podman';
+    /** @var null|string */
+    private static $containerId;
 
     public static function setUpBeforeClass(): void
     {
         parent::setUpBeforeClass();
-        exec('which docker', $output, $code);
+        exec('which podman', $output, $code);
+        if ($code > 0) {
+            exec('which docker', $output, $code);
+            static::$containerCommand = 'docker';
+        }
         if ($code > 0) {
             static::$skip = true;
 
             return;
         }
-        exec('docker run --rm -p 16379:6379 -d redislabs/redisearch:latest');
+        $output = [];
+        exec(static::$containerCommand.' run --rm -p 16379:6379 -d redislabs/redisearch:latest', $output, $code);
+        static::$containerId = reset($output) ?: null;
         Client\AbstractClient::$disableNotice = true;
     }
 
     public static function tearDownAfterClass(): void
     {
         parent::tearDownAfterClass();
-        if (static::$skip) {
+        if (static::$skip || !is_string(static::$containerId)) {
             return;
         }
-        exec('docker ps --filter publish=16379 -q', $output);
-        foreach ($output as $container) {
-            exec('docker stop '.escapeshellarg($container), $output);
-        }
+        exec(static::$containerCommand.' stop '.escapeshellarg(static::$containerId), $output);
         Client\AbstractClient::$disableNotice = false;
     }
 
@@ -141,7 +149,7 @@ protected function setUp(): void
     {
         parent::setUp();
         if (static::$skip) {
-            static::markTestSkipped('Docker is missing');
+            static::markTestSkipped('Podman/Docker is missing');
         }
     }
 

From 4876826f25f99e2e6c416344aacfb9ebe9c04eb7 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Fri, 26 Nov 2021 21:25:33 +0100
Subject: [PATCH 26/60] Update phpstan

---
 CHANGELOG.md                                       |  1 +
 composer.json                                      |  2 +-
 phpstan.neon                                       |  1 +
 .../Command/AggregateCommand/ReduceOption.php      |  2 +-
 tests/Redis/Command/Option/BaseOptionTestTrait.php |  3 ---
 .../Command/Option/CustomValidatorOptionTest.php   |  3 ---
 tests/Redis/Command/Option/FlagOptionTest.php      |  3 ---
 tests/Redis/Command/Option/GroupedOptionTest.php   |  3 ---
 tests/Redis/Command/Option/NamedOptionTest.php     |  3 ---
 tests/Redis/Command/Option/NamelessOptionTest.php  |  3 ---
 tests/Redis/Command/Option/NotEmptyOptionTest.php  |  3 ---
 tests/Redis/Command/Option/NumberedOptionTest.php  |  6 ------
 tests/integration/DockerTest.php                   | 14 +++++++-------
 13 files changed, 11 insertions(+), 36 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b01c64e..cf7ffde 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 
 - `APPLY` option of Aggregate command is now dependent of the last inserted option (if the option have a meaning for `APPLY`)
+- (dev) Update `phpstan` (from `0.12` to `1.2`)
 
 ### Fixed
 
diff --git a/composer.json b/composer.json
index 177edef..c001eab 100644
--- a/composer.json
+++ b/composer.json
@@ -32,7 +32,7 @@
         "insolita/unused-scanner": "^2.3",
         "php-parallel-lint/php-parallel-lint": "^1.3",
         "phpmd/phpmd": "^2.10",
-        "phpstan/phpstan": "^0.12.92",
+        "phpstan/phpstan": "^1.2.0",
         "phpunit/phpunit": "^8.5 || ^9.3",
         "predis/predis": "^1.1",
         "ptrofimov/tinyredisclient": "^1.1",
diff --git a/phpstan.neon b/phpstan.neon
index ee1a995..45f1cab 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -10,6 +10,7 @@ parameters:
         - '#Method MacFJA\\RediSearch\\Redis\\Command\\\w+::getLanguageOptions\(\) should return#'
         - '#uses generic trait MacFJA\\RediSearch\\Redis\\Command\\Option\\DecoratedOptionTrait but does not specify its types: T#'
         - '#Call to an undefined method MacFJA\\RediSearch\\Redis\\Command\\Option\\CustomValidatorOption::setValue\(\).#'
+        - '#Method MacFJA\\RediSearch\\tests\\Redis.+::dataProvider\(\) return type has no value type specified in iterable type (array|Generator).#'
         -
             message: '#Call to an undefined method MacFJA\\RediSearch\\Redis\\Command\\Option\\CommandOption::set\w+\(\).#'
             path: src/Redis/Command/Option/GroupedOption.php
\ No newline at end of file
diff --git a/src/Redis/Command/AggregateCommand/ReduceOption.php b/src/Redis/Command/AggregateCommand/ReduceOption.php
index 070a27b..7f57973 100644
--- a/src/Redis/Command/AggregateCommand/ReduceOption.php
+++ b/src/Redis/Command/AggregateCommand/ReduceOption.php
@@ -60,7 +60,7 @@ public function isValid(): bool
     }
 
     /**
-     * @return array<string,null|array|string>
+     * @return array<string,null|array<float|int|string>|string>
      */
     public function getOptionData()
     {
diff --git a/tests/Redis/Command/Option/BaseOptionTestTrait.php b/tests/Redis/Command/Option/BaseOptionTestTrait.php
index fe102a6..f030899 100644
--- a/tests/Redis/Command/Option/BaseOptionTestTrait.php
+++ b/tests/Redis/Command/Option/BaseOptionTestTrait.php
@@ -54,8 +54,5 @@ public function testGetOptionData(CommandOption $inputOption, $expected): void
         self::assertEquals($expected, $inputOption->getOptionData());
     }
 
-    /**
-     * @return Generator<array>
-     */
     abstract public function dataProvider(string $testName): Generator;
 }
diff --git a/tests/Redis/Command/Option/CustomValidatorOptionTest.php b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
index 5034a4f..58e3bc7 100644
--- a/tests/Redis/Command/Option/CustomValidatorOptionTest.php
+++ b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
@@ -61,9 +61,6 @@ public function testShorthands(): void
         static::assertFalse($invalidList->isValid());
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $invalidMinLengthOption = new CustomValidatorOption(new NamelessOption('foo'), new Length(4));
diff --git a/tests/Redis/Command/Option/FlagOptionTest.php b/tests/Redis/Command/Option/FlagOptionTest.php
index b3b6c36..182b995 100644
--- a/tests/Redis/Command/Option/FlagOptionTest.php
+++ b/tests/Redis/Command/Option/FlagOptionTest.php
@@ -58,9 +58,6 @@ public function testGetName(): void
         static::assertEquals('BAR', $option->getName());
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $defaultValueFlag = new FlagOption('FOO');
diff --git a/tests/Redis/Command/Option/GroupedOptionTest.php b/tests/Redis/Command/Option/GroupedOptionTest.php
index b45ef71..9c38843 100644
--- a/tests/Redis/Command/Option/GroupedOptionTest.php
+++ b/tests/Redis/Command/Option/GroupedOptionTest.php
@@ -122,9 +122,6 @@ public function testSetLockOptionData(): void
         $option->setDataOfOption('lock', 'bar');
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $emptyGroup = new GroupedOption([], []);
diff --git a/tests/Redis/Command/Option/NamedOptionTest.php b/tests/Redis/Command/Option/NamedOptionTest.php
index 306dad8..d5ee69f 100644
--- a/tests/Redis/Command/Option/NamedOptionTest.php
+++ b/tests/Redis/Command/Option/NamedOptionTest.php
@@ -57,9 +57,6 @@ public function testGetName(): void
         static::assertEquals('BAR', $option->getName());
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $defaultValueOption = new NamedOption('FOO');
diff --git a/tests/Redis/Command/Option/NamelessOptionTest.php b/tests/Redis/Command/Option/NamelessOptionTest.php
index ab37d32..de436f2 100644
--- a/tests/Redis/Command/Option/NamelessOptionTest.php
+++ b/tests/Redis/Command/Option/NamelessOptionTest.php
@@ -48,9 +48,6 @@ public function testGetSetValue(): void
         static::assertEquals(10, $option->getValue());
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $defaultValueOption = new NamelessOption();
diff --git a/tests/Redis/Command/Option/NotEmptyOptionTest.php b/tests/Redis/Command/Option/NotEmptyOptionTest.php
index b4a6ceb..d5c4f5c 100644
--- a/tests/Redis/Command/Option/NotEmptyOptionTest.php
+++ b/tests/Redis/Command/Option/NotEmptyOptionTest.php
@@ -62,9 +62,6 @@ public function testIsValid2(): void
         static::assertSame([], $option->render());
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $namelessOption = new NotEmptyOption(new NamelessOption('foo'));
diff --git a/tests/Redis/Command/Option/NumberedOptionTest.php b/tests/Redis/Command/Option/NumberedOptionTest.php
index 30173c7..4cde462 100644
--- a/tests/Redis/Command/Option/NumberedOptionTest.php
+++ b/tests/Redis/Command/Option/NumberedOptionTest.php
@@ -60,9 +60,6 @@ public function testGetName(): void
         static::assertEquals('BAR', $option->getName());
     }
 
-    /**
-     * @return Generator<array>
-     */
     public function dataProvider(string $testName): Generator
     {
         $noArgumentsOption = new NumberedOption('FOO');
@@ -94,9 +91,6 @@ public function dataProvider(string $testName): Generator
                 yield [$withMultipleArgumentOption, ['FOO', 2, 'bar', 'foobar']];
 
                 break;
-
-            default:
-                return;
         }
     }
 }
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index ad6686a..5175d4f 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -122,33 +122,33 @@ public static function setUpBeforeClass(): void
         exec('which podman', $output, $code);
         if ($code > 0) {
             exec('which docker', $output, $code);
-            static::$containerCommand = 'docker';
+            self::$containerCommand = 'docker';
         }
         if ($code > 0) {
-            static::$skip = true;
+            self::$skip = true;
 
             return;
         }
         $output = [];
-        exec(static::$containerCommand.' run --rm -p 16379:6379 -d redislabs/redisearch:latest', $output, $code);
-        static::$containerId = reset($output) ?: null;
+        exec(self::$containerCommand.' run --rm -p 16379:6379 -d redislabs/redisearch:latest', $output, $code);
+        self::$containerId = reset($output) ?: null;
         Client\AbstractClient::$disableNotice = true;
     }
 
     public static function tearDownAfterClass(): void
     {
         parent::tearDownAfterClass();
-        if (static::$skip || !is_string(static::$containerId)) {
+        if (self::$skip || !is_string(self::$containerId)) {
             return;
         }
-        exec(static::$containerCommand.' stop '.escapeshellarg(static::$containerId), $output);
+        exec(self::$containerCommand.' stop '.escapeshellarg(self::$containerId), $output);
         Client\AbstractClient::$disableNotice = false;
     }
 
     protected function setUp(): void
     {
         parent::setUp();
-        if (static::$skip) {
+        if (self::$skip) {
             static::markTestSkipped('Podman/Docker is missing');
         }
     }

From fd3bff9302a6b2c5997bc5bafcdf3efb46ca8721 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 28 Nov 2021 13:31:49 +0100
Subject: [PATCH 27/60] Release 2.0.2

---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf7ffde..cd2693d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.0.2]
+
 ### Added
 
 - `reset` method for builder (`\MacFJA\RediSearch\IndexBuilder` and `\MacFJA\RediSearch\Query\Builder`)
@@ -145,7 +147,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.2...HEAD
+[2.0.2]: https://github.com/MacFJA/php-redisearch/compare/2.0.1...2.0.2
 [2.0.1]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...2.0.1
 [2.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/2.0.0
 [Unreleased v1.x]: https://github.com/MacFJA/php-redisearch/compare/1.4.0...1.x

From bb056d8f5bc3d4cc030fb0b22dbf57ead3e5a302 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 28 Nov 2021 19:26:00 +0100
Subject: [PATCH 28/60] Add support for JSON document

---
 CHANGELOG.md                                  |   8 ++
 Makefile                                      |   6 +-
 src/IndexBuilder.php                          |  12 ++
 src/Redis/Command/AddFieldOptionTrait.php     |  60 ++++++++++
 src/Redis/Command/Create.php                  |   9 +-
 .../CreateCommandJSONFieldOption.php          |  27 +++++
 .../Command/CreateCommand/JSONFieldOption.php |  69 +++++++++++
 tests/IndexBuilderTest.php                    |  40 +++++++
 .../Redis/Command/AddFieldOptionTraitTest.php |  10 ++
 tests/Redis/Command/AggregateTest.php         |   7 ++
 tests/Redis/Command/CreateTest.php            |  68 ++++++++++-
 .../Command/Option/JSONFieldOptionTest.php    | 108 ++++++++++++++++++
 12 files changed, 418 insertions(+), 6 deletions(-)
 create mode 100644 src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php
 create mode 100644 src/Redis/Command/CreateCommand/JSONFieldOption.php
 create mode 100644 tests/Redis/Command/Option/JSONFieldOptionTest.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd2693d..571ba24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Added
+
+- Support from JSON document (require RediSearch 2.2)
+
+### Fixed
+
+- (dev) Code coverage with XDebug 3
+
 ## [2.0.2]
 
 ### Added
diff --git a/Makefile b/Makefile
index e9185a6..bd42908 100644
--- a/Makefile
+++ b/Makefile
@@ -23,18 +23,18 @@ test-with-integration: | vendor
 
 coverage: | vendor
 	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
-	$(COMPOSER) exec -v phpunit -- --coverage-text --color
+	XDEBUG_MODE=coverage $(COMPOSER) exec -v phpunit -- --coverage-text --color
 
 coverage-with-integration: | vendor
 	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
-	$(COMPOSER) exec -v phpunit -- --group default,integration --coverage-text --color
+	XDEBUG_MODE=coverage $(COMPOSER) exec -v phpunit -- --group default,integration --coverage-text --color
 
 integration-test: | vendor
 	$(COMPOSER) exec -v phpunit -- --group integration
 
 integration-coverage: | vendor
 	@if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi
-	$(COMPOSER) exec -v phpunit -- --group integration --coverage-text --color
+	XDEBUG_MODE=coverage $(COMPOSER) exec -v phpunit -- --group integration --coverage-text --color
 
 validation: fix-code analyze test-with-integration coverage-with-integration
 
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 6a1dfe5..31215a0 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -34,6 +34,7 @@
 use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
 use MacFJA\RediSearch\Redis\Command\Create;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
 use RuntimeException;
 use function strlen;
 
@@ -84,6 +85,10 @@
  * @method IndexBuilder withAddedNumericField(string $name, bool $sortable = false, bool $noIndex = false)
  * @method IndexBuilder withAddedGeoField(string $name, bool $noIndex = false)
  * @method IndexBuilder withAddedTagField(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false)
+ * @method IndexBuilder withAddedJSONTextField(string $path, string $attribute, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false)
+ * @method IndexBuilder withAddedJSONNumericField(string $path, string $attribute, bool $sortable = false, bool $noIndex = false)
+ * @method IndexBuilder withAddedJSONGeoField(string $path, string $attribute, bool $noIndex = false)
+ * @method IndexBuilder withAddedJSONTagField(string $path, string $attribute, ?string $separator = null, bool $sortable = false, bool $noIndex = false)
  *
  * @SuppressWarnings(PHPMD.TooManyFields)
  */
@@ -185,6 +190,13 @@ public function addField(CreateCommandFieldOption $option): self
         return $this;
     }
 
+    public function addJSONField(string $path, CreateCommandFieldOption $option): self
+    {
+        $this->fields[$option->getFieldName()] = new JSONFieldOption($path, $option);
+
+        return $this;
+    }
+
     /**
      * @return mixed|string
      */
diff --git a/src/Redis/Command/AddFieldOptionTrait.php b/src/Redis/Command/AddFieldOptionTrait.php
index 389d6ec..451c1e6 100644
--- a/src/Redis/Command/AddFieldOptionTrait.php
+++ b/src/Redis/Command/AddFieldOptionTrait.php
@@ -25,6 +25,7 @@
 use function get_class;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
@@ -75,6 +76,54 @@ public function addTagField(string $name, ?string $separator = null, bool $sorta
         );
     }
 
+    public function addJSONTextField(string $path, string $attribute, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false): self
+    {
+        return $this->addJSONField(
+            $path,
+            (new TextFieldOption())
+                ->setField($attribute)
+                ->setNoStem($noStem)
+                ->setWeight($weight)
+                ->setPhonetic($phonetic)
+                ->setSortable($sortable)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addJSONNumericField(string $path, string $attribute, bool $sortable = false, bool $noIndex = false): self
+    {
+        return $this->addJSONField(
+            $path,
+            (new NumericFieldOption())
+                ->setField($attribute)
+                ->setSortable($sortable)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addJSONGeoField(string $path, string $attribute, bool $noIndex = false): self
+    {
+        return $this->addJSONField(
+            $path,
+            (new GeoFieldOption())
+                ->setField($attribute)
+                ->setNoIndex($noIndex)
+        );
+    }
+
+    public function addJSONTagField(string $path, string $attribute, ?string $separator = null, bool $sortable = false, bool $noIndex = false, bool $caseSensitive = false): self
+    {
+        return $this->addJSONField(
+            $path,
+            (new TagFieldOption())
+                ->setField($attribute)
+                ->setSeparator($separator)
+                ->setCaseSensitive($caseSensitive)
+                ->setSortable($sortable)
+                ->setNoIndex($noIndex)
+        );
+    }
+
     public function addField(CreateCommandFieldOption $option): self
     {
         if (!($this instanceof AbstractCommand)) {
@@ -85,4 +134,15 @@ public function addField(CreateCommandFieldOption $option): self
 
         return $this;
     }
+
+    public function addJSONField(string $path, CreateCommandFieldOption $option): self
+    {
+        if (!($this instanceof AbstractCommand)) {
+            throw new BadMethodCallException('This method is not callable in '.get_class($this));
+        }
+
+        $this->options['fields'][$option->getFieldName()] = new JSONFieldOption($path, $option);
+
+        return $this;
+    }
 }
diff --git a/src/Redis/Command/Create.php b/src/Redis/Command/Create.php
index 5687404..6e840c9 100644
--- a/src/Redis/Command/Create.php
+++ b/src/Redis/Command/Create.php
@@ -37,7 +37,10 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
     {
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
-            'structure' => CV::allowedValues(new NamedOption('ON', null, '>=2.0.0'), ['HASH']),
+            'structure' => [
+                CV::allowedValues(new NamedOption('ON', null, '>=2.0.0 <2.2.0'), ['HASH']),
+                CV::allowedValues(new NamedOption('ON', null, '>=2.2.0'), ['HASH', 'JSON']),
+            ],
             'prefixes' => new NotEmptyOption(new NumberedOption('PREFIX', null, '>=2.0.0')),
             'filter' => new NamedOption('FILTER', null, '>=2.0.0'),
             'default_lang' => $this->getLanguageOptions(),
@@ -67,7 +70,9 @@ public function setIndex(string $index): self
 
     public function setStructure(string $structureType = 'HASH'): self
     {
-        $this->options['structure']->setValue($structureType);
+        array_walk($this->options['structure'], static function (CV $option) use ($structureType): void {
+            $option->setValue($structureType);
+        });
 
         return $this;
     }
diff --git a/src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php b/src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php
new file mode 100644
index 0000000..a41eb67
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+interface CreateCommandJSONFieldOption extends CreateCommandFieldOption
+{
+    public function getJSONPath(): string;
+}
diff --git a/src/Redis/Command/CreateCommand/JSONFieldOption.php b/src/Redis/Command/CreateCommand/JSONFieldOption.php
new file mode 100644
index 0000000..8f3a9b2
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/JSONFieldOption.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+
+class JSONFieldOption extends AbstractCommandOption implements CreateCommandJSONFieldOption
+{
+    /** @var CreateCommandFieldOption */
+    private $decorated;
+    /** @var string */
+    private $path;
+
+    public function __construct(string $path, CreateCommandFieldOption $decorated)
+    {
+        parent::__construct('>=2.2.0');
+        $this->path = $path;
+        $this->decorated = $decorated;
+    }
+
+    public function isValid(): bool
+    {
+        return !empty($this->path) && $this->decorated->isValid();
+    }
+
+    public function getOptionData()
+    {
+        return array_merge(['path' => $this->path], $this->decorated->getOptionData());
+    }
+
+    public function getVersionConstraint(): string
+    {
+        return '>=2.2.0';
+    }
+
+    public function getFieldName(): string
+    {
+        return $this->decorated->getFieldName();
+    }
+
+    public function getJSONPath(): string
+    {
+        return $this->path;
+    }
+
+    protected function doRender(?string $version): array
+    {
+        return array_merge([$this->path, 'AS'], $this->decorated->render($version));
+    }
+}
diff --git a/tests/IndexBuilderTest.php b/tests/IndexBuilderTest.php
index 6587e5c..96a98bb 100644
--- a/tests/IndexBuilderTest.php
+++ b/tests/IndexBuilderTest.php
@@ -48,6 +48,7 @@
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
  * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait
@@ -252,6 +253,45 @@ public function testAllAdd(): void
         static::assertEquals($createCommand, $builder->getCommand());
     }
 
+    public function testAllJsonAdd(): void
+    {
+        $createCommand = new Create();
+
+        $builder = new IndexBuilder();
+
+        $builder->setIndex('city');
+        $createCommand->setIndex('city');
+
+        $oldBuilder = $builder;
+        $builder = $builder->addPrefixes('city-', 'c-');
+        static::assertNotEquals($createCommand, $builder->getCommand());
+        $createCommand->setPrefixes('city-', 'c-');
+        static::assertEquals($createCommand, $builder->getCommand());
+        static::assertSame($oldBuilder, $builder);
+
+        $builder->addStopWords('hello', 'world');
+        $createCommand->setStopWords('hello', 'world');
+
+        $builder->addJSONField('$.city.name', (new TextFieldOption())->setField('name'));
+        $createCommand->addJSONTextField('$.city.name', 'name');
+
+        $builder->addJSONField('$.city.country', (new TextFieldOption())->setField('country'));
+        $createCommand->addJSONTextField('$.city.country', 'country');
+
+        $builder->addJSONTextField('$.city.continent', 'continent');
+        $createCommand->addJSONTextField('$.city.continent', 'continent');
+
+        $builder->addJSONNumericField('$.city.population', 'population');
+        $createCommand->addJSONNumericField('$.city.population', 'population');
+
+        $builder->addJSONGeoField('$.city.gps', 'gps');
+        $createCommand->addJSONGeoField('$.city.gps', 'gps');
+
+        $builder->addJSONTagField('$.city.languages', 'languages');
+        $createCommand->addJSONTagField('$.city.languages', 'languages');
+        static::assertEquals($createCommand, $builder->getCommand());
+    }
+
     /**
      * @medium
      */
diff --git a/tests/Redis/Command/AddFieldOptionTraitTest.php b/tests/Redis/Command/AddFieldOptionTraitTest.php
index 0b5ade7..78187d7 100644
--- a/tests/Redis/Command/AddFieldOptionTraitTest.php
+++ b/tests/Redis/Command/AddFieldOptionTraitTest.php
@@ -51,6 +51,16 @@ public function testWrongParent(): void
         $command->addNumericField('foo');
     }
 
+    public function testWrongParent2(): void
+    {
+        $command = new FakeAddFieldOptionTraitClass1();
+
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('This method is not callable in '.FakeAddFieldOptionTraitClass1::class);
+
+        $command->addJSONNumericField('$.foo', 'foo');
+    }
+
     public function testValidParent(): void
     {
         $command = new FakeAddFieldOptionTraitClass2([]);
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index 23a93ae..afd4192 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -60,6 +60,13 @@ public function testGetId(): void
         static::assertSame('FT.AGGREGATE', $command->getId());
     }
 
+    public function testGetIndex(): void
+    {
+        $command = new Aggregate();
+        $command->setIndex('idx');
+        static::assertSame('idx', $command->getIndex());
+    }
+
     public function testMultiReduce(): void
     {
         $group = new GroupByOption(['@text1']);
diff --git a/tests/Redis/Command/CreateTest.php b/tests/Redis/Command/CreateTest.php
index 00c028d..923ab4b 100644
--- a/tests/Redis/Command/CreateTest.php
+++ b/tests/Redis/Command/CreateTest.php
@@ -21,8 +21,11 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use InvalidArgumentException;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\Create;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
 use PHPUnit\Framework\TestCase;
 
@@ -30,9 +33,9 @@
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\Create
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
- *
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
@@ -191,4 +194,67 @@ public function testUnNormalizedForm(): void
         $textField->setSortable(false);
         static::assertSame(['idx', 'SCHEMA', 'foo', 'TEXT'], $command->getArguments());
     }
+
+    public function testJSONStructure(): void
+    {
+        $expected = [
+            'userIdx', 'ON', 'JSON', 'SCHEMA', '$.user.name', 'AS', 'name', 'TEXT', '$.user.tag', 'AS', 'country', 'TAG',
+        ];
+        $command = new Create('2.2.0');
+        $command
+            ->setIndex('userIdx')
+            ->setStructure('JSON')
+            ->addField(new JSONFieldOption('$.user.name', (new TextFieldOption())->setField('name')))
+            ->addField(new JSONFieldOption('$.user.tag', (new TagFieldOption())->setField('country')))
+        ;
+
+        static::assertSame($expected, $command->getArguments());
+    }
+
+    public function testJSONStructureShortHand(): void
+    {
+        $expected = [
+            'userIdx', 'ON', 'JSON', 'SCHEMA', '$.user.name', 'AS', 'name', 'TEXT', '$.user.tag', 'AS', 'country', 'TAG',
+        ];
+        $command = new Create('2.2.0');
+        $command
+            ->setIndex('userIdx')
+            ->setStructure('JSON')
+            ->addJSONField('$.user.name', (new TextFieldOption())->setField('name'))
+            ->addJSONField('$.user.tag', (new TagFieldOption())->setField('country'))
+        ;
+
+        static::assertSame($expected, $command->getArguments());
+    }
+
+    public function testJSONStructureShortHand2(): void
+    {
+        $expected = [
+            'userIdx', 'ON', 'JSON', 'SCHEMA', '$.user.name', 'AS', 'name', 'TEXT', '$.user.tag', 'AS', 'country', 'TAG',
+        ];
+        $command = new Create('2.2.0');
+        $command
+            ->setIndex('userIdx')
+            ->setStructure('JSON')
+            ->addJSONTextField('$.user.name', 'name')
+            ->addJSONTagField('$.user.tag', 'country')
+        ;
+
+        static::assertSame($expected, $command->getArguments());
+    }
+
+    public function testJSONStructureWrongVersion(): void
+    {
+        $command = new Create('2.0.0');
+        $command
+            ->setIndex('userIdx')
+            ->setStructure('JSON')
+            ->addJSONTextField('$.user.name', 'name')
+            ->addJSONTagField('$.user.tag', 'country')
+        ;
+
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('Missing command option: fields');
+        $command->getArguments();
+    }
 }
diff --git a/tests/Redis/Command/Option/JSONFieldOptionTest.php b/tests/Redis/Command/Option/JSONFieldOptionTest.php
new file mode 100644
index 0000000..90ba780
--- /dev/null
+++ b/tests/Redis/Command/Option/JSONFieldOptionTest.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class JSONFieldOptionTest extends TestCase
+{
+    public function testIsValid(): void
+    {
+        $option = new TextFieldOption();
+        $json = new JSONFieldOption('$.data', $option);
+
+        static::assertFalse($option->isValid());
+        static::assertFalse($json->isValid());
+
+        $option->setField('data');
+        $json = new JSONFieldOption('', $option);
+        static::assertTrue($option->isValid());
+        static::assertFalse($json->isValid());
+
+        $json = new JSONFieldOption('$.data', $option);
+        static::assertTrue($option->isValid());
+        static::assertTrue($json->isValid());
+    }
+
+    public function testGetOptionData(): void
+    {
+        $option = new TextFieldOption();
+        $option->setField('data');
+        $json = new JSONFieldOption('$.data', $option);
+
+        $expected = [
+            'path' => '$.data',
+        ] + $option->getOptionData();
+
+        static::assertSame($expected, $json->getOptionData());
+    }
+
+    public function testGetVersionConstraint(): void
+    {
+        $option = new TextFieldOption();
+        $json = new JSONFieldOption('$.data', $option);
+
+        static::assertSame('>=2.2.0', $json->getVersionConstraint());
+    }
+
+    public function testGetFieldName(): void
+    {
+        $option = new TextFieldOption();
+        $option->setField('data');
+        $json = new JSONFieldOption('$.data', $option);
+
+        static::assertSame('data', $json->getFieldName());
+    }
+
+    public function testGetJSONPath(): void
+    {
+        $option = new TextFieldOption();
+        $json = new JSONFieldOption('$.data', $option);
+
+        static::assertSame('$.data', $json->getJSONPath());
+    }
+
+    public function testDoRender(): void
+    {
+        $option = new TextFieldOption();
+        $option->setField('data');
+        $json = new JSONFieldOption('$.data', $option);
+
+        static::assertSame(['$.data', 'AS', 'data', 'TEXT'], $json->render());
+    }
+}

From 51f899d4c0288d09aa19d4091ae077b98bdef749 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 15 Jan 2022 13:56:35 +0100
Subject: [PATCH 29/60] Add GitHub Actions

---
 .gitattributes                  |  1 +
 .github/workflows/validate.yaml | 46 +++++++++++++++++++++++++++++++++
 CHANGELOG.md                    |  2 ++
 composer.json                   |  7 +++++
 4 files changed, 56 insertions(+)
 create mode 100644 .github/workflows/validate.yaml

diff --git a/.gitattributes b/.gitattributes
index eab2151..4722e63 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -24,3 +24,4 @@ Makefile        text
 /*.xml.dist       export-ignore
 /phpstan.neon     export-ignore
 /psalm.xml        export-ignore
+/.github          export-ignore
diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml
new file mode 100644
index 0000000..ed1224f
--- /dev/null
+++ b/.github/workflows/validate.yaml
@@ -0,0 +1,46 @@
+on:
+  pull_request:
+    branches: [ $default-branch ]
+
+jobs:
+  analyze:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - &setup-php
+        name: Setup PHP with PECL extension
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: '8.0'
+          extensions: intl, mbstring
+          coverage: xdebug
+      - &composer-cache
+        name: Cache Composer packages
+        id: composer-cache
+        uses: actions/cache@v2
+        with:
+          path: vendor
+          key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json,') }}
+          restore-keys: |
+            ${{ runner.os }}-php-
+      - name: Validate code
+        run: make analyze
+
+  unit-test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - *setup-php
+      - *composer-cache
+      - name: Run unit test
+        id: make-test
+        run: make test
+
+  coverage:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - *setup-php
+      - *composer-cache
+      - name: Code coverage
+        run: make coverage
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 571ba24..f83da11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Support from JSON document (require RediSearch 2.2)
+- (dev) GitHub Actions
 
 ### Fixed
 
 - (dev) Code coverage with XDebug 3
+- (dev) Defined list of allowed Composer plugins
 
 ## [2.0.2]
 
diff --git a/composer.json b/composer.json
index c001eab..03a0581 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
         "respect/validation": "^2.0"
     },
     "require-dev": {
+        "ext-mbstring": "*",
         "amphp/redis": "^1.0",
         "cheprasov/php-redis-client": "^1.10",
         "colinmollenhour/credis": "^1.12",
@@ -54,6 +55,12 @@
         "ptrofimov/tinyredisclient": "To use TinyRedisClient implementation",
         "redisent/redisent": "To use Redisent implementation"
     },
+    "config": {
+        "allow-plugins": {
+            "composer/package-versions-deprecated": true,
+            "ergebnis/composer-normalize": true
+        }
+    },
     "autoload": {
         "psr-4": {
             "MacFJA\\RediSearch\\": "src/"

From 16869cfa01ed09130ebfc3c9a320b4d095e271f5 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 15 Jan 2022 17:06:31 +0100
Subject: [PATCH 30/60] Change CI trigger

---
 .github/workflows/validate.yaml | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml
index ed1224f..ca17b32 100644
--- a/.github/workflows/validate.yaml
+++ b/.github/workflows/validate.yaml
@@ -1,6 +1,4 @@
-on:
-  pull_request:
-    branches: [ $default-branch ]
+on: pull_request
 
 jobs:
   analyze:

From 231129f9cd4d1684ff6f9e1c3abec5bda43f4658 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 15 Jan 2022 17:09:43 +0100
Subject: [PATCH 31/60] Remove YAML anchors

---
 .github/workflows/validate.yaml | 40 ++++++++++++++++++++++++++++-----
 1 file changed, 34 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml
index ca17b32..91fcf0c 100644
--- a/.github/workflows/validate.yaml
+++ b/.github/workflows/validate.yaml
@@ -5,14 +5,14 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - &setup-php
+      - #&setup-php
         name: Setup PHP with PECL extension
         uses: shivammathur/setup-php@v2
         with:
           php-version: '8.0'
           extensions: intl, mbstring
           coverage: xdebug
-      - &composer-cache
+      - #&composer-cache
         name: Cache Composer packages
         id: composer-cache
         uses: actions/cache@v2
@@ -28,8 +28,22 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - *setup-php
-      - *composer-cache
+      - #*setup-php
+        name: Setup PHP with PECL extension
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: '8.0'
+          extensions: intl, mbstring
+          coverage: xdebug
+      - #*composer-cache
+        name: Cache Composer packages
+        id: composer-cache
+        uses: actions/cache@v2
+        with:
+          path: vendor
+          key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json,') }}
+          restore-keys: |
+            ${{ runner.os }}-php-
       - name: Run unit test
         id: make-test
         run: make test
@@ -38,7 +52,21 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - *setup-php
-      - *composer-cache
+      - #*setup-php
+        name: Setup PHP with PECL extension
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: '8.0'
+          extensions: intl, mbstring
+          coverage: xdebug
+      - #*composer-cache
+        name: Cache Composer packages
+        id: composer-cache
+        uses: actions/cache@v2
+        with:
+          path: vendor
+          key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json,') }}
+          restore-keys: |
+            ${{ runner.os }}-php-
       - name: Code coverage
         run: make coverage
\ No newline at end of file

From b1fc0a29b76dca9e7841a4c270d7a1d3cd286c9f Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 15 Jan 2022 17:01:36 +0100
Subject: [PATCH 32/60] Fix PaginatedResponse index

---
 CHANGELOG.md                             | 2 ++
 src/Redis/Response/CursorResponse.php    | 4 ++++
 src/Redis/Response/PaginatedResponse.php | 6 +++++-
 3 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f83da11..bcfc70e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,11 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Support from JSON document (require RediSearch 2.2)
 - (dev) GitHub Actions
+- Protection against division by 0
 
 ### Fixed
 
 - (dev) Code coverage with XDebug 3
 - (dev) Defined list of allowed Composer plugins
+- Fix `PaginatedResponse` index (now start at 0, and have a linear progression)
 
 ## [2.0.2]
 
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index b81f052..231a96f 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -106,6 +106,10 @@ public function getPageSize(): int
 
     public function getPageCount(): int
     {
+        if (0 === $this->size) {
+            return 0;
+        }
+
         return (int) ceil($this->totalCount / $this->size);
     }
 
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index b3d38f0..6b8a893 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -96,6 +96,10 @@ public function getPageSize(): int
 
     public function getPageCount(): int
     {
+        if (0 === $this->getPageSize()) {
+            return 0;
+        }
+
         return (int) ceil($this->totalCount / $this->getPageSize());
     }
 
@@ -105,7 +109,7 @@ public function key()
             return 0;
         }
 
-        return (int) ceil(($this->lastCommand->getOffset() ?? 0) / $this->getPageSize()) + 1;
+        return (int) floor(($this->lastCommand->getOffset() ?? 0) / $this->getPageSize());
     }
 
     public function valid()

From bad89b313559406957ba0d00a6cf27a9b32fc646 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 15 Jan 2022 18:27:31 +0100
Subject: [PATCH 33/60] Version 2.1.0

---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcfc70e..695cf07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.1.0]
+
 ### Added
 
 - Support from JSON document (require RediSearch 2.2)
@@ -159,7 +161,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.0.2...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.1.0...HEAD
+[2.1.0]: https://github.com/MacFJA/php-redisearch/compare/2.0.2...2.1.0
 [2.0.2]: https://github.com/MacFJA/php-redisearch/compare/2.0.1...2.0.2
 [2.0.1]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...2.0.1
 [2.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/2.0.0

From 320589fe0768ca86f25a78dffc95e1fa384d1de8 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Fri, 4 Feb 2022 20:42:54 +0100
Subject: [PATCH 34/60] Fix Summary and Highlight always active [#26]

---
 CHANGELOG.md                                     |  6 ++++++
 src/Redis/Command/Search.php                     |  2 ++
 .../Command/SearchCommand/HighlightOption.php    |  4 ++--
 .../Command/SearchCommand/SummarizeOption.php    |  4 ++--
 tests/Redis/Command/SearchTest.php               | 16 ++++++++++++++++
 5 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 695cf07..37f60ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Fixed
+
+- Summarize Search option enabled by default (without possibility to disable it) ([Issue#26])
+- Highlight Search option enabled by default (without possibility to disable it) ([Issue#26])
+
 ## [2.1.0]
 
 ### Added
@@ -181,6 +186,7 @@ First version
 [Issue#11]: https://github.com/MacFJA/php-redisearch/issues/11
 [Issue#12]: https://github.com/MacFJA/php-redisearch/issues/12
 [Issue#16]: https://github.com/MacFJA/php-redisearch/issues/16
+[Issue#26]: https://github.com/MacFJA/php-redisearch/issues/26
 [PR#1]: https://github.com/MacFJA/php-redisearch/pull/1
 [PR#3]: https://github.com/MacFJA/php-redisearch/pull/3
 [PR#8]: https://github.com/MacFJA/php-redisearch/pull/8
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 1d86c48..aeb8208 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -146,6 +146,7 @@ public function setWithSortKeys(bool $active = true): self
     public function setSummarize(?array $fields = null, ?int $fragmentCount = null, ?int $fragmentSize = null, ?string $separator = null): self
     {
         $this->options['summarize']
+            ->setDataOfOption('type', true)
             ->setDataOfOption('fields', $fields)
             ->setDataOfOption('frags', $fragmentCount)
             ->setDataOfOption('len', $fragmentSize)
@@ -163,6 +164,7 @@ public function setSummarize(?array $fields = null, ?int $fragmentCount = null,
     public function setHighlight(?array $fields, ?string $openTag, ?string $closeTag): self
     {
         $this->options['highlight']
+            ->setDataOfOption('type', true)
             ->setDataOfOption('fields', $fields)
             ->setTags($openTag, $closeTag)
         ;
diff --git a/src/Redis/Command/SearchCommand/HighlightOption.php b/src/Redis/Command/SearchCommand/HighlightOption.php
index b81da3f..72f6527 100644
--- a/src/Redis/Command/SearchCommand/HighlightOption.php
+++ b/src/Redis/Command/SearchCommand/HighlightOption.php
@@ -32,14 +32,14 @@ class HighlightOption extends GroupedOption
     public function __construct()
     {
         parent::__construct([
-            'type' => new FlagOption('HIGHLIGHT', true, '>=2.0.0'),
+            'type' => new FlagOption('HIGHLIGHT', false, '>=2.0.0'),
             'fields' => new NotEmptyOption(new NumberedOption('FIELDS', null, '>=2.0.0')),
             'tag' => new GroupedOption([
                 'type' => new FlagOption('TAGS', true, '>=2.0.0'),
                 'open' => new NamelessOption(null, '>=2.0.0'),
                 'close' => new NamelessOption(null, '>=2.0.0'),
             ], ['type', 'open', 'close'], ['type'], '>=2.0.0'),
-        ], ['type'], ['type'], '>=2.0.0');
+        ], ['type'], [], '>=2.0.0');
     }
 
     public function setTags(?string $open, ?string $close): self
diff --git a/src/Redis/Command/SearchCommand/SummarizeOption.php b/src/Redis/Command/SearchCommand/SummarizeOption.php
index 6ebebba..fa8cfd4 100644
--- a/src/Redis/Command/SearchCommand/SummarizeOption.php
+++ b/src/Redis/Command/SearchCommand/SummarizeOption.php
@@ -33,11 +33,11 @@ class SummarizeOption extends GroupedOption
     public function __construct()
     {
         parent::__construct([
-            'type' => new FlagOption('SUMMARIZE', true, '>=2.0.0'),
+            'type' => new FlagOption('SUMMARIZE', false, '>=2.0.0'),
             'fields' => new NotEmptyOption(new NumberedOption('FIELDS', null, '>=2.0.0')),
             'frags' => CV::isNumeric(new NamedOption('FRAGS', null, '>=2.0.0')),
             'len' => CV::isNumeric(new NamedOption('LEN', null, '>=2.0.0')),
             'separator' => new NamedOption('SEPARATOR', null, '>=2.0.0'),
-        ], ['type'], ['type'], '>=2.0.0');
+        ], ['type'], [], '>=2.0.0');
     }
 }
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index 47a50d3..440be97 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -126,4 +126,20 @@ public function testFullOption(): void
             'LIMIT', 12, 10,
         ], $command->getArguments());
     }
+
+    public function testNoSummarizeAndNoHighlight(): void
+    {
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setLimit(12, 10)
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'LIMIT', 12, 10,
+        ], $command->getArguments());
+    }
 }

From 1cfcb151fb30a74f7869695b7b137bcdb4c07947 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 5 Feb 2022 14:34:18 +0100
Subject: [PATCH 35/60] Fix error when parsing response from Search with
 NOCONTENT flag

---
 CHANGELOG.md                 | 1 +
 src/Redis/Command/Search.php | 4 +++-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37f60ea..788eb4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Summarize Search option enabled by default (without possibility to disable it) ([Issue#26])
 - Highlight Search option enabled by default (without possibility to disable it) ([Issue#26])
+- Fix error when parsing response from Search with NOCONTENT flag
 
 ## [2.1.0]
 
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index aeb8208..97d0604 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -327,7 +327,9 @@ public function parseResponse($data)
         $chunkSize = 2 // Hash + fields
             + (true === $useScores ? 1 : 0)
             + (true === $usePayloads ? 1 : 0)
-            + (true === $useSortKeys ? 1 : 0);
+            + (true === $useSortKeys ? 1 : 0)
+            - (true === $noContent ? 1 : 0)
+        ;
 
         $documents = array_chunk($data, $chunkSize);
 

From 3f97b12487ddee9ab32e7ae192046e16e127d6f0 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 5 Feb 2022 17:39:25 +0100
Subject: [PATCH 36/60] Add more test

---
 tests/Redis/Command/SearchTest.php | 92 ++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index 440be97..944dba2 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -24,6 +24,8 @@
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\Search;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
+use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
+use MacFJA\RediSearch\Redis\Response\SearchResponseItem;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -38,6 +40,8 @@
  *
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
  *
+ * @covers \MacFJA\RediSearch\Redis\Response\PaginatedResponse
+ *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
@@ -142,4 +146,92 @@ public function testNoSummarizeAndNoHighlight(): void
             'LIMIT', 12, 10,
         ], $command->getArguments());
     }
+
+    public function testParsingResponseNoContent(): void
+    {
+        $redisResponse = [
+            3,
+            'hash_1',
+            'hash_2',
+            'hash_3',
+        ];
+
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
+        $command->setQuery('*');
+        $command->setNoContent();
+
+        $expectedResponse = new PaginatedResponse($command, 3, [
+            new SearchResponseItem('hash_1'),
+            new SearchResponseItem('hash_2'),
+            new SearchResponseItem('hash_3'),
+        ]);
+
+        $actualResponse = $command->parseResponse($redisResponse);
+
+        static::assertEquals($expectedResponse, $actualResponse);
+    }
+
+    public function testParsingResponseScore(): void
+    {
+        $redisResponse = [
+            3,
+            'hash_1',
+            '0.741',
+            ['field', '1'],
+            'hash_2',
+            '0.654',
+            ['field', '2'],
+            'hash_3',
+            '0.258',
+            ['field', '3'],
+        ];
+
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
+        $command->setQuery('*');
+        $command->setWithScores();
+
+        $expectedResponse = new PaginatedResponse($command, 3, [
+            new SearchResponseItem('hash_1', ['field' => '1'], 0.741),
+            new SearchResponseItem('hash_2', ['field' => '2'], 0.654),
+            new SearchResponseItem('hash_3', ['field' => '3'], 0.258),
+        ]);
+
+        $actualResponse = $command->parseResponse($redisResponse);
+
+        static::assertEquals($expectedResponse, $actualResponse);
+    }
+
+    public function testParsingResponseScoreAndPayload(): void
+    {
+        $redisResponse = [
+            3,
+            'hash_1',
+            '0.741',
+            null,
+            ['field', '1'],
+            'hash_2',
+            '0.654',
+            null,
+            ['field', '2', 'other', 'field'],
+            'hash_3',
+            '0.258',
+            'third',
+            ['field', '3'],
+        ];
+
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
+        $command->setQuery('*');
+        $command->setWithScores();
+        $command->setWithPayloads();
+
+        $expectedResponse = new PaginatedResponse($command, 3, [
+            new SearchResponseItem('hash_1', ['field' => '1'], 0.741),
+            new SearchResponseItem('hash_2', ['field' => '2', 'other' => 'field'], 0.654),
+            new SearchResponseItem('hash_3', ['field' => '3'], 0.258, 'third'),
+        ]);
+
+        $actualResponse = $command->parseResponse($redisResponse);
+
+        static::assertEquals($expectedResponse, $actualResponse);
+    }
 }

From a8171a884d668bd31917313ab386bbc7eacf1f4b Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 5 Feb 2022 16:36:58 +0100
Subject: [PATCH 37/60] Fix PaginatedResponse

---
 CHANGELOG.md                             |  1 +
 src/Redis/Response/PaginatedResponse.php | 13 +++++++++----
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 788eb4c..ce1a1c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Summarize Search option enabled by default (without possibility to disable it) ([Issue#26])
 - Highlight Search option enabled by default (without possibility to disable it) ([Issue#26])
 - Fix error when parsing response from Search with NOCONTENT flag
+- Make PaginatedResponse `valid` iterator function work
 
 ## [2.1.0]
 
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index 6b8a893..3687772 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -109,14 +109,19 @@ public function key()
             return 0;
         }
 
-        return (int) floor(($this->lastCommand->getOffset() ?? 0) / $this->getPageSize());
+        return (int) floor(($this->lastCommand->getOffset() ?? $this->requestedOffset ?? 0) / $this->getPageSize());
     }
 
     public function valid()
     {
-        return $this->requestedOffset >= 0
-            && $this->requestedSize > 0
-            && $this->requestedOffset < $this->totalCount;
+        return (
+                null === $this->requestedOffset
+                && null === $this->requestedSize
+            ) || (
+                $this->requestedOffset >= 0
+                && $this->requestedSize > 0
+                && $this->requestedOffset < $this->totalCount
+            );
     }
 
     public function rewind(): void

From e80451ed01c1fd40abee752398ff8a953f9ba2a3 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 12 Mar 2022 19:40:37 +0100
Subject: [PATCH 38/60] Release 2.1.1

---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce1a1c0..6365b16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.1.1]
+
 ### Fixed
 
 - Summarize Search option enabled by default (without possibility to disable it) ([Issue#26])
@@ -168,7 +170,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.1.0...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.1.1...HEAD
+[2.1.1]: https://github.com/MacFJA/php-redisearch/compare/2.1.0...2.1.1
 [2.1.0]: https://github.com/MacFJA/php-redisearch/compare/2.0.2...2.1.0
 [2.0.2]: https://github.com/MacFJA/php-redisearch/compare/2.0.1...2.0.2
 [2.0.1]: https://github.com/MacFJA/php-redisearch/compare/2.0.0...2.0.1

From 6cdffa6a4fec15f31c3cb1112c9ccbe762636be3 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 27 Mar 2022 19:02:23 +0200
Subject: [PATCH 39/60] Update dev tools version (PHP-CS-Fixer + PHPStan)

---
 .php-cs-fixer.dist.php                        | 41 +++++++++++++++----
 .unused.dist.php                              | 24 +++++------
 CHANGELOG.md                                  |  4 ++
 Makefile                                      |  6 +--
 composer.json                                 | 22 +++++-----
 src/Index.php                                 |  3 ++
 src/IndexBuilder.php                          | 18 ++++++++
 src/Query/Builder/EncapsulationGroup.php      |  3 ++
 src/Query/Builder/FieldFacet.php              |  1 +
 src/Query/Builder/Fuzzy.php                   |  1 +
 src/Redis/Client.php                          |  2 -
 src/Redis/Client/AbstractClient.php           |  2 -
 src/Redis/Client/ClientFacade.php             |  3 --
 src/Redis/Command/AbstractCommand.php         |  2 +
 .../AggregateCommand/GroupByOption.php        |  1 +
 .../Command/AggregateCommand/SortByOption.php |  2 +
 .../Command/CreateCommand/JSONFieldOption.php |  1 +
 .../Command/Option/AbstractCommandOption.php  |  1 +
 src/Redis/Command/Option/FlagOption.php       |  1 +
 src/Redis/Command/Option/GroupedOption.php    |  3 ++
 src/Redis/Command/Option/NumberedOption.php   |  1 +
 .../Command/SearchCommand/FilterOption.php    |  4 ++
 .../Command/SearchCommand/LimitOption.php     |  1 +
 src/Redis/Command/SpellCheck.php              |  6 ++-
 .../Command/SpellCheckCommand/TermsOption.php |  1 +
 src/Redis/Response/CursorResponse.php         |  9 ++++
 src/Redis/Response/PaginatedResponse.php      |  4 ++
 src/Redis/Response/SpellCheckResponseItem.php |  1 +
 src/Suggestions.php                           |  3 ++
 tests/Query/EscaperTest.php                   |  1 +
 .../Option/CustomValidatorOptionTest.php      | 18 ++++++++
 tests/Redis/Command/Option/FlagOptionTest.php |  6 +++
 .../Command/Option/GroupedOptionTest.php      | 27 ++++++++++++
 .../Redis/Command/Option/NamedOptionTest.php  |  3 ++
 .../Command/Option/NamelessOptionTest.php     |  6 +++
 .../Command/Option/NotEmptyOptionTest.php     | 37 ++++++++++++++++-
 .../Command/Option/NumberedOptionTest.php     |  9 ++++
 tests/Redis/InitializerTest.php               |  6 +++
 tests/Redis/Response/InfoResponseTest.php     |  1 +
 tests/integration/DockerTest.php              |  2 +
 tests/psalm/stubs/phpiredis.php               |  4 ++
 41 files changed, 245 insertions(+), 46 deletions(-)

diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index c6d01b1..a5e9991 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -1,10 +1,32 @@
 <?php
+
+declare(strict_types=1);
+
 /*
+ * Copyright MacFJA
+ *
+ * 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.
+ *
+ * ----------
+ *
  * This document has been generated with
  * https://mlocati.github.io/php-cs-fixer-configurator/#version:3.0.0|configurator
  * you can change this configuration by importing this file.
  */
+
 $config = new PhpCsFixer\Config();
+
 return $config
     ->setRiskyAllowed(true)
     ->setRules([
@@ -17,15 +39,15 @@
         '@PhpCsFixer' => true,
         'combine_nested_dirname' => true,
         'dir_constant' => true,
-        'global_namespace_import' => ['import_functions'=>true],
-        'header_comment' => ['comment_type'=>'comment','header'=>"Copyright MacFJA\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\nrights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the\nSoftware.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\nWARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",'location'=>'after_declare_strict'],
+        'global_namespace_import' => ['import_functions' => true],
+        'header_comment' => ['comment_type' => 'comment', 'header' => "Copyright MacFJA\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\nrights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the\nSoftware.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\nWARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", 'location' => 'after_declare_strict'],
         'implode_call' => true,
         'logical_operators' => true,
         'modernize_types_casting' => true,
         'native_function_invocation' => true,
-        'no_alias_functions' => ['sets'=>['@all']],
+        'no_alias_functions' => ['sets' => ['@all']],
         'no_php4_constructor' => true,
-        'no_superfluous_phpdoc_tags' => ['allow_mixed'=>true,'remove_inheritdoc'=>true],
+        'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => true],
         'no_unreachable_default_argument_value' => true,
         'no_useless_sprintf' => true,
         'php_unit_dedicate_assert' => true,
@@ -43,9 +65,10 @@
         'ternary_to_elvis_operator' => true,
         'void_return' => true,
     ])
-    ->setFinder(PhpCsFixer\Finder::create()
-        ->exclude('vendor')
-        ->in(__DIR__.'/src')
-        ->in(__DIR__.'/tests')
+    ->setFinder(
+        PhpCsFixer\Finder::create()
+            ->exclude('vendor')
+            ->in(__DIR__.'/src')
+            ->in(__DIR__.'/tests')
     )
-    ;
\ No newline at end of file
+    ;
diff --git a/.unused.dist.php b/.unused.dist.php
index 39cc3d7..a9c78bd 100644
--- a/.unused.dist.php
+++ b/.unused.dist.php
@@ -1,24 +1,20 @@
 
 <?php
 
-$projectPath = __DIR__ . '/';
+$projectPath = __DIR__.'/';
 
 $scanDirectories = [
-    $projectPath . '/src/',
-    $projectPath . '/tests/',
+    $projectPath.'/src/',
+    $projectPath.'/tests/',
 ];
 
 return [
-    /**
-     * Required params
-     **/
-    'composerJsonPath' => $projectPath . '/composer.json',
-    'vendorPath' => $projectPath . '/vendor/',
+    // Required params
+    'composerJsonPath' => $projectPath.'/composer.json',
+    'vendorPath' => $projectPath.'/vendor/',
     'scanDirectories' => $scanDirectories,
 
-    /**
-     * Optional params
-     **/
+    // Optional params
     'skipPackages' => [
         'phpmd/phpmd', // QA tool
         'vimeo/psalm', // QA tool
@@ -28,10 +24,10 @@
         'enlightn/security-checker', // QA tool
         'php-parallel-lint/php-parallel-lint', // QA tool
         'sebastian/phpcpd', // QA tool
-        'ukko/phpredis-phpdoc' // Stubs
+        'ukko/phpredis-phpdoc', // Stubs
     ],
     'excludeDirectories' => [],
     'scanFiles' => [],
     'extensions' => ['*.php'],
-    'requireDev' => true
-];
\ No newline at end of file
+    'requireDev' => true,
+];
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6365b16..a03f621 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Changed
+
+- (dev) Update version and rules of PHP-CS-Fixer and PHPStan
+
 ## [2.1.1]
 
 ### Fixed
diff --git a/Makefile b/Makefile
index bd42908..ac983c9 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
 
 analyze: | vendor
 	$(COMPOSER) exec -v parallel-lint -- src
-	$(COMPOSER) exec -v php-cs-fixer -- fix --dry-run
+	$(COMPOSER) exec -v php-cs-fixer -- fix --dry-run -v
 	$(COMPOSER) exec -v unused_scanner -- .unused.dist.php
 	$(COMPOSER) exec -v security-checker -- security:check
 	$(COMPOSER) exec -v phpcpd -- --fuzzy src
@@ -13,7 +13,7 @@ analyze: | vendor
 
 fix-code: | vendor
 	$(COMPOSER) normalize
-	$(COMPOSER) exec -v php-cs-fixer -- fix
+	$(COMPOSER) exec -v php-cs-fixer -- fix -v
 
 test: | vendor
 	$(COMPOSER) exec -v phpunit
@@ -55,4 +55,4 @@ ifneq ($(shell command -v composer > /dev/null ; echo $$?), 0)
   COMPOSER=php composer.phar
 else
   COMPOSER=composer
-endif
\ No newline at end of file
+endif
diff --git a/composer.json b/composer.json
index 03a0581..e6ca3be 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,8 @@
 {
     "name": "macfja/redisearch",
-    "type": "library",
     "description": "PHP Client for RediSearch",
+    "license": "MIT",
+    "type": "library",
     "keywords": [
         "Redis",
         "RediSearch",
@@ -9,12 +10,15 @@
         "Suggestion"
     ],
     "readme": "README.md",
-    "license": "MIT",
     "authors": [
         {
             "name": "MacFJA"
         }
     ],
+    "support": {
+        "issues": "https://github.com/MacFJA/php-redisearch/issues",
+        "source": "https://github.com/MacFJA/php-redisearch"
+    },
     "require": {
         "php": "^7.2 || ^8.0",
         "ext-intl": "*",
@@ -55,12 +59,6 @@
         "ptrofimov/tinyredisclient": "To use TinyRedisClient implementation",
         "redisent/redisent": "To use Redisent implementation"
     },
-    "config": {
-        "allow-plugins": {
-            "composer/package-versions-deprecated": true,
-            "ergebnis/composer-normalize": true
-        }
-    },
     "autoload": {
         "psr-4": {
             "MacFJA\\RediSearch\\": "src/"
@@ -71,8 +69,10 @@
             "MacFJA\\RediSearch\\tests\\": "tests/"
         }
     },
-    "support": {
-        "issues": "https://github.com/MacFJA/php-redisearch/issues",
-        "source": "https://github.com/MacFJA/php-redisearch"
+    "config": {
+        "allow-plugins": {
+            "composer/package-versions-deprecated": true,
+            "ergebnis/composer-normalize": true
+        }
     }
 }
diff --git a/src/Index.php b/src/Index.php
index eb4c225..02d269e 100644
--- a/src/Index.php
+++ b/src/Index.php
@@ -44,10 +44,13 @@ class Index
 {
     /** @var Client */
     private $client;
+
     /** @var InfoResponse */
     private $info;
+
     /** @var string */
     private $index;
+
     /** @var string */
     private $version;
 
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 31215a0..78674b7 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -97,40 +97,58 @@ class IndexBuilder
     use AddFieldOptionTrait;
 
     private const IS_ARRAY = ['prefixes', 'stopWords', 'fields'];
+
     /** @var null|string */
     private $index;
+
     /** @var null|string */
     private $structure;
+
     /** @var array<string> */
     private $prefixes = [];
+
     /** @var null|string */
     private $filter;
+
     /** @var null|string */
     private $defaultLanguage;
+
     /** @var null|string */
     private $languageField;
+
     /** @var null|float */
     private $defaultScore;
+
     /** @var null|string */
     private $scoreField;
+
     /** @var null|string */
     private $payloadField;
+
     /** @var bool */
     private $maxTextFields = false;
+
     /** @var null|int */
     private $temporary;
+
     /** @var bool */
     private $noOffsets = false;
+
     /** @var bool */
     private $noHighLight = false;
+
     /** @var bool */
     private $noFields = false;
+
     /** @var bool */
     private $noFrequencies = false;
+
     /** @var bool */
     private $skipInitialScan = false;
+
     /** @var null|array<string> */
     private $stopWords;
+
     /** @var array<CreateCommandFieldOption> */
     private $fields = [];
 
diff --git a/src/Query/Builder/EncapsulationGroup.php b/src/Query/Builder/EncapsulationGroup.php
index 656d535..9c7c871 100644
--- a/src/Query/Builder/EncapsulationGroup.php
+++ b/src/Query/Builder/EncapsulationGroup.php
@@ -25,10 +25,13 @@ class EncapsulationGroup implements QueryElement, QueryElementDecorator
 {
     /** @var string */
     private $open;
+
     /** @var string */
     private $close;
+
     /** @var bool */
     private $omitIfNoSpace;
+
     /** @var QueryElement */
     private $element;
 
diff --git a/src/Query/Builder/FieldFacet.php b/src/Query/Builder/FieldFacet.php
index 2df1e19..2a22685 100644
--- a/src/Query/Builder/FieldFacet.php
+++ b/src/Query/Builder/FieldFacet.php
@@ -29,6 +29,7 @@ class FieldFacet implements QueryElement
 {
     /** @var array<string> */
     private $fields;
+
     /** @var QueryElement */
     private $element;
 
diff --git a/src/Query/Builder/Fuzzy.php b/src/Query/Builder/Fuzzy.php
index ffce965..4fbc423 100644
--- a/src/Query/Builder/Fuzzy.php
+++ b/src/Query/Builder/Fuzzy.php
@@ -28,6 +28,7 @@ class Fuzzy implements QueryElement
 {
     /** @var string */
     private $word;
+
     /** @var int */
     private $distance;
 
diff --git a/src/Redis/Client.php b/src/Redis/Client.php
index ee94663..e1452b0 100644
--- a/src/Redis/Client.php
+++ b/src/Redis/Client.php
@@ -43,8 +43,6 @@ public function execute(Command $command);
     public function executeRaw(...$args);
 
     /**
-     * @param Command ...$commands
-     *
      * @return array<mixed>
      */
     public function pipeline(Command ...$commands): array;
diff --git a/src/Redis/Client/AbstractClient.php b/src/Redis/Client/AbstractClient.php
index 193d9ed..3856f3e 100644
--- a/src/Redis/Client/AbstractClient.php
+++ b/src/Redis/Client/AbstractClient.php
@@ -41,8 +41,6 @@ public function pipeline(Command ...$commands): array
     }
 
     /**
-     * @param Command ...$commands
-     *
      * @return array<mixed>
      */
     abstract protected function doPipeline(Command ...$commands): array;
diff --git a/src/Redis/Client/ClientFacade.php b/src/Redis/Client/ClientFacade.php
index b213c46..d16a269 100644
--- a/src/Redis/Client/ClientFacade.php
+++ b/src/Redis/Client/ClientFacade.php
@@ -53,9 +53,6 @@ public function getClient($redis): Client
     {
         /** @var Client $factory */
         foreach ($this->factories as $factory) {
-            if (!is_subclass_of($factory, Client::class)) {
-                continue;
-            }
             if ($factory::supports($redis)) {
                 return $factory::make($redis);
             }
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index e9cedd5..c9855f3 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -34,10 +34,12 @@ abstract class AbstractCommand implements Command
 {
     public const MAX_IMPLEMENTED_VERSION = '2.0.12';
     public const MIN_IMPLEMENTED_VERSION = '2.0.0';
+
     /**
      * @var array<array<CommandOption>|CommandOption|mixed>
      */
     protected $options;
+
     /** @var string */
     private $rediSearchVersion;
 
diff --git a/src/Redis/Command/AggregateCommand/GroupByOption.php b/src/Redis/Command/AggregateCommand/GroupByOption.php
index 7985702..4441c59 100644
--- a/src/Redis/Command/AggregateCommand/GroupByOption.php
+++ b/src/Redis/Command/AggregateCommand/GroupByOption.php
@@ -31,6 +31,7 @@ class GroupByOption extends AbstractCommandOption
 
     /** @var array<ReduceOption> */
     private $reducers = [];
+
     /** @var array<string> */
     private $fields;
 
diff --git a/src/Redis/Command/AggregateCommand/SortByOption.php b/src/Redis/Command/AggregateCommand/SortByOption.php
index e1a5155..ea9b8b0 100644
--- a/src/Redis/Command/AggregateCommand/SortByOption.php
+++ b/src/Redis/Command/AggregateCommand/SortByOption.php
@@ -29,8 +29,10 @@
 class SortByOption extends AbstractCommandOption
 {
     use PrefixFieldName;
+
     /** @var array<string,null|string> */
     private $fields = [];
+
     /** @var null|int */
     private $max;
 
diff --git a/src/Redis/Command/CreateCommand/JSONFieldOption.php b/src/Redis/Command/CreateCommand/JSONFieldOption.php
index 8f3a9b2..9b2588b 100644
--- a/src/Redis/Command/CreateCommand/JSONFieldOption.php
+++ b/src/Redis/Command/CreateCommand/JSONFieldOption.php
@@ -27,6 +27,7 @@ class JSONFieldOption extends AbstractCommandOption implements CreateCommandJSON
 {
     /** @var CreateCommandFieldOption */
     private $decorated;
+
     /** @var string */
     private $path;
 
diff --git a/src/Redis/Command/Option/AbstractCommandOption.php b/src/Redis/Command/Option/AbstractCommandOption.php
index 30f5fc8..cd37c61 100644
--- a/src/Redis/Command/Option/AbstractCommandOption.php
+++ b/src/Redis/Command/Option/AbstractCommandOption.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command\Option;
 
 use Composer\Semver\Semver;
+use function is_scalar;
 use UnexpectedValueException;
 
 abstract class AbstractCommandOption implements CommandOption
diff --git a/src/Redis/Command/Option/FlagOption.php b/src/Redis/Command/Option/FlagOption.php
index c315582..992c216 100644
--- a/src/Redis/Command/Option/FlagOption.php
+++ b/src/Redis/Command/Option/FlagOption.php
@@ -25,6 +25,7 @@ class FlagOption extends AbstractCommandOption
 {
     /** @var string */
     private $name;
+
     /** @var bool */
     private $active;
 
diff --git a/src/Redis/Command/Option/GroupedOption.php b/src/Redis/Command/Option/GroupedOption.php
index 49503bf..9b91a1e 100644
--- a/src/Redis/Command/Option/GroupedOption.php
+++ b/src/Redis/Command/Option/GroupedOption.php
@@ -25,6 +25,7 @@
 use BadMethodCallException;
 use function in_array;
 use function is_bool;
+use function is_scalar;
 use OutOfRangeException;
 
 class GroupedOption extends AbstractCommandOption
@@ -33,8 +34,10 @@ class GroupedOption extends AbstractCommandOption
 
     /** @var array<string,CommandOption> */
     private $options;
+
     /** @var array<string> */
     private $required;
+
     /** @var array<string> */
     private $locked;
 
diff --git a/src/Redis/Command/Option/NumberedOption.php b/src/Redis/Command/Option/NumberedOption.php
index 9bd725a..f52d998 100644
--- a/src/Redis/Command/Option/NumberedOption.php
+++ b/src/Redis/Command/Option/NumberedOption.php
@@ -29,6 +29,7 @@ class NumberedOption extends AbstractCommandOption
 {
     /** @var string */
     private $name;
+
     /** @var null|array<float|int|string> */
     private $arguments;
 
diff --git a/src/Redis/Command/SearchCommand/FilterOption.php b/src/Redis/Command/SearchCommand/FilterOption.php
index 618bb2e..c4c0570 100644
--- a/src/Redis/Command/SearchCommand/FilterOption.php
+++ b/src/Redis/Command/SearchCommand/FilterOption.php
@@ -29,12 +29,16 @@ class FilterOption extends AbstractCommandOption
 
     /** @var string */
     private $fieldName;
+
     /** @var null|float */
     private $min;
+
     /** @var null|float */
     private $max;
+
     /** @var bool */
     private $isMaxInclusive;
+
     /** @var bool */
     private $isMinInclusive;
 
diff --git a/src/Redis/Command/SearchCommand/LimitOption.php b/src/Redis/Command/SearchCommand/LimitOption.php
index e9e585f..d819fd0 100644
--- a/src/Redis/Command/SearchCommand/LimitOption.php
+++ b/src/Redis/Command/SearchCommand/LimitOption.php
@@ -28,6 +28,7 @@ class LimitOption extends AbstractCommandOption
 {
     /** @var null|int */
     private $offset;
+
     /** @var null|int */
     private $size;
 
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
index b2a196d..ed30225 100644
--- a/src/Redis/Command/SpellCheck.php
+++ b/src/Redis/Command/SpellCheck.php
@@ -100,8 +100,12 @@ public function parseResponse($data)
             $misspelled = array_shift($group);
             $suggestions = reset($group);
             $suggestions = array_column($suggestions, 0, 1);
+            $castedSuggestions = [];
+            foreach ($suggestions as $term => $score) {
+                $castedSuggestions[(string) $term] = (float) $score;
+            }
 
-            return new SpellCheckResponseItem($misspelled, $suggestions);
+            return new SpellCheckResponseItem($misspelled, $castedSuggestions);
         }, $data);
 
         return array_filter($items);
diff --git a/src/Redis/Command/SpellCheckCommand/TermsOption.php b/src/Redis/Command/SpellCheckCommand/TermsOption.php
index fcdf953..3b69f8d 100644
--- a/src/Redis/Command/SpellCheckCommand/TermsOption.php
+++ b/src/Redis/Command/SpellCheckCommand/TermsOption.php
@@ -28,6 +28,7 @@ class TermsOption extends AbstractCommandOption
 {
     /** @var bool */
     private $isExclusion = false;
+
     /** @var string */
     private $dictionary;
 
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index 231a96f..a477a46 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -35,20 +35,28 @@ class CursorResponse implements Response, Iterator
 {
     /** @var array<AggregateResponseItem> */
     private $items;
+
     /** @var int */
     private $totalCount;
+
     /** @var Client */
     private $client;
+
     /** @var bool */
     private $doNext = false;
+
     /** @var int */
     private $cursorId;
+
     /** @var int */
     private $size;
+
     /** @var int */
     private $offset = 0;
+
     /** @var string */
     private $index;
+
     /** @var string */
     private $redisVersion;
 
@@ -82,6 +90,7 @@ public function current()
             $cursorRead->setIndex($this->index);
             $cursorRead->setCursorId($this->cursorId);
             $cursorRead->setCount($this->getPageSize());
+
             /** @var CursorResponse $next */
             $next = $this->client->execute($cursorRead);
             $this->cursorId = $next->cursorId;
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index 3687772..d139526 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -36,15 +36,19 @@ class PaginatedResponse implements Response, Iterator, Countable
 {
     /** @var array<AggregateResponseItem>|array<SearchResponseItem> */
     private $items;
+
     /** @var PaginatedCommand */
     private $lastCommand;
+
     /** @var int */
     private $totalCount;
+
     /** @var Client */
     private $client;
 
     /** @var null|int */
     private $requestedOffset;
+
     /** @var null|int */
     private $requestedSize;
 
diff --git a/src/Redis/Response/SpellCheckResponseItem.php b/src/Redis/Response/SpellCheckResponseItem.php
index 55df80a..bd2c464 100644
--- a/src/Redis/Response/SpellCheckResponseItem.php
+++ b/src/Redis/Response/SpellCheckResponseItem.php
@@ -30,6 +30,7 @@ class SpellCheckResponseItem
 {
     /** @var string */
     private $term;
+
     /** @var array<string,float> */
     private $suggestions;
 
diff --git a/src/Suggestions.php b/src/Suggestions.php
index ad785ec..91c0732 100644
--- a/src/Suggestions.php
+++ b/src/Suggestions.php
@@ -39,10 +39,13 @@ class Suggestions
 {
     /** @var string */
     private $dictionary;
+
     /** @var Client */
     private $client;
+
     /** @var int */
     private $length;
+
     /** @var string */
     private $version;
 
diff --git a/tests/Query/EscaperTest.php b/tests/Query/EscaperTest.php
index 40f5ce7..bf3ac8e 100644
--- a/tests/Query/EscaperTest.php
+++ b/tests/Query/EscaperTest.php
@@ -40,6 +40,7 @@ class EscaperTest extends \PHPUnit\Framework\TestCase
         'testEscapeOptional' => 'optional',
         'testEscapeNothing' => 'input',
     ];
+
     /**
      * @var array<array<string>>
      */
diff --git a/tests/Redis/Command/Option/CustomValidatorOptionTest.php b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
index 58e3bc7..925d40d 100644
--- a/tests/Redis/Command/Option/CustomValidatorOptionTest.php
+++ b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
@@ -72,42 +72,60 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$invalidMinLengthOption, 'foo'];
+
                 yield [$validMinLengthOption, 'foobar'];
+
                 yield [$emptyMinLengthOption, null];
 
                 break;
 
             case 'testIsValid':
                 yield [$invalidMinLengthOption, false];
+
                 yield [$validMinLengthOption, true];
+
                 yield [$emptyMinLengthOption, false];
 
                 break;
 
             case 'testRender':
                 yield [$invalidMinLengthOption, []];
+
                 yield [$validMinLengthOption, ['foobar']];
+
                 yield [$emptyMinLengthOption, []];
 
                 break;
 
             case 'testIsCompatible':
                 yield [$validMinLengthOption, '1.0.0', true];
+
                 yield [$validMinLengthOption, '1.5.0', true];
+
                 yield [$validMinLengthOption, '2.0.0', true];
+
                 yield [$validMinLengthOption, '2.5.0', true];
+
                 yield [$validMinLengthOption, 'foo', false];
 
                 yield [$lowValidMinLengthOption, '1.0.0', true];
+
                 yield [$lowValidMinLengthOption, '1.5.0', true];
+
                 yield [$lowValidMinLengthOption, '2.0.0', true];
+
                 yield [$lowValidMinLengthOption, '2.5.0', true];
+
                 yield [$lowValidMinLengthOption, 'foo', false];
 
                 yield [$highValidMinLengthOption, '1.0.0', false];
+
                 yield [$highValidMinLengthOption, '1.5.0', false];
+
                 yield [$highValidMinLengthOption, '2.0.0', true];
+
                 yield [$highValidMinLengthOption, '2.5.0', true];
+
                 yield [$highValidMinLengthOption, 'foo', false];
 
                 break;
diff --git a/tests/Redis/Command/Option/FlagOptionTest.php b/tests/Redis/Command/Option/FlagOptionTest.php
index 182b995..1b0c551 100644
--- a/tests/Redis/Command/Option/FlagOptionTest.php
+++ b/tests/Redis/Command/Option/FlagOptionTest.php
@@ -67,21 +67,27 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$defaultValueFlag, false];
+
                 yield [$notActiveFlag, false];
+
                 yield [$activeFlag, true];
 
                 break;
 
             case 'testIsValid':
                 yield [$defaultValueFlag, true];
+
                 yield [$notActiveFlag, true];
+
                 yield [$activeFlag, true];
 
                 break;
 
             case 'testRender':
                 yield [$defaultValueFlag, []];
+
                 yield [$notActiveFlag, []];
+
                 yield [$activeFlag, ['BAR']];
 
                 break;
diff --git a/tests/Redis/Command/Option/GroupedOptionTest.php b/tests/Redis/Command/Option/GroupedOptionTest.php
index 9c38843..71033c2 100644
--- a/tests/Redis/Command/Option/GroupedOptionTest.php
+++ b/tests/Redis/Command/Option/GroupedOptionTest.php
@@ -137,51 +137,78 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$emptyGroup, []];
+
                 yield [$noRequiredGroup, ['foo' => new NamelessOption()]];
+
                 yield [$emptyRequiredGroup, []];
+
                 yield [$noValueRequireGroup, ['foo' => new NamelessOption()]];
+
                 yield [$valueRequireGroup, ['foo' => new NamelessOption('FOO')]];
+
                 yield [$valueRequireGroup2, ['foo' => new NamelessOption(0)]];
+
                 yield [$valueLockGroup, ['foo' => new NamelessOption('FOO')]];
 
                 break;
 
             case 'testIsValid':
                 yield [$emptyGroup, true];
+
                 yield [$noRequiredGroup, true];
+
                 yield [$emptyRequiredGroup, false];
+
                 yield [$noValueRequireGroup, false];
+
                 yield [$valueRequireGroup, true];
+
                 yield [$valueRequireGroup2, true];
+
                 yield [$valueLockGroup, true];
 
                 break;
 
             case 'testRender':
                 yield [$emptyGroup, []];
+
                 yield [$noRequiredGroup, []];
+
                 yield [$emptyRequiredGroup, []];
+
                 yield [$noValueRequireGroup, []];
+
                 yield [$valueRequireGroup, ['FOO']];
+
                 yield [$valueRequireGroup2, [0]];
+
                 yield [$valueLockGroup, ['FOO']];
 
                 break;
 
             case 'testIsCompatible':
                 yield [$emptyGroup, '1.0.0', true];
+
                 yield [$emptyGroup, '1.5.0', true];
+
                 yield [$emptyGroup, '2.0.0', true];
+
                 yield [$emptyGroup, '2.5.0', true];
 
                 yield [$lowVersionGroup, '1.0.0', true];
+
                 yield [$lowVersionGroup, '1.5.0', true];
+
                 yield [$lowVersionGroup, '2.0.0', true];
+
                 yield [$lowVersionGroup, '2.5.0', true];
 
                 yield [$highVersionGroup, '1.0.0', false];
+
                 yield [$highVersionGroup, '1.5.0', false];
+
                 yield [$highVersionGroup, '2.0.0', true];
+
                 yield [$highVersionGroup, '2.5.0', true];
 
                 break;
diff --git a/tests/Redis/Command/Option/NamedOptionTest.php b/tests/Redis/Command/Option/NamedOptionTest.php
index d5ee69f..d7bce60 100644
--- a/tests/Redis/Command/Option/NamedOptionTest.php
+++ b/tests/Redis/Command/Option/NamedOptionTest.php
@@ -65,18 +65,21 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$defaultValueOption, null];
+
                 yield [$withValueOption, 'bar'];
 
                 break;
 
             case 'testIsValid':
                 yield [$defaultValueOption, false];
+
                 yield [$withValueOption, true];
 
                 break;
 
             case 'testRender':
                 yield [$defaultValueOption, []];
+
                 yield [$withValueOption, ['FOO', 'bar']];
 
                 break;
diff --git a/tests/Redis/Command/Option/NamelessOptionTest.php b/tests/Redis/Command/Option/NamelessOptionTest.php
index de436f2..c6f8166 100644
--- a/tests/Redis/Command/Option/NamelessOptionTest.php
+++ b/tests/Redis/Command/Option/NamelessOptionTest.php
@@ -57,21 +57,27 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$defaultValueOption, null];
+
                 yield [$withValueOption, 'FOO'];
+
                 yield [$emptyValueOption, ''];
 
                 break;
 
             case 'testIsValid':
                 yield [$defaultValueOption, false];
+
                 yield [$withValueOption, true];
+
                 yield [$emptyValueOption, true];
 
                 break;
 
             case 'testRender':
                 yield [$defaultValueOption, []];
+
                 yield [$withValueOption, ['FOO']];
+
                 yield [$emptyValueOption, ['']];
 
                 break;
diff --git a/tests/Redis/Command/Option/NotEmptyOptionTest.php b/tests/Redis/Command/Option/NotEmptyOptionTest.php
index d5c4f5c..f4243be 100644
--- a/tests/Redis/Command/Option/NotEmptyOptionTest.php
+++ b/tests/Redis/Command/Option/NotEmptyOptionTest.php
@@ -27,6 +27,7 @@
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
 use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
@@ -40,7 +41,7 @@
  *
  * @internal
  */
-class NotEmptyOptionTest extends \PHPUnit\Framework\TestCase
+class NotEmptyOptionTest extends TestCase
 {
     use BaseOptionTestTrait;
     use TestIsCompatibleTrait;
@@ -79,58 +80,92 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$namelessOption, 'foo'];
+
                 yield [$namedOption, 'bar'];
+
                 yield [$numberedOption, ['bar', 'foobar']];
+
                 yield [$emptyNamelessOption, null];
+
                 yield [$emptyNamedOption, null];
+
                 yield [$emptyNumberedOption, []];
+
                 yield [$nullNumberedOption, null];
+
                 yield [$trueFlagOption, true];
+
                 yield [$falseFlagOption, false];
 
                 break;
 
             case 'testIsValid':
                 yield [$namelessOption, true];
+
                 yield [$namedOption, true];
+
                 yield [$numberedOption, true];
+
                 yield [$emptyNamelessOption, false];
+
                 yield [$emptyNamedOption, false];
+
                 yield [$emptyNumberedOption, false];
+
                 yield [$trueFlagOption, true];
+
                 yield [$falseFlagOption, false];
 
                 break;
 
             case 'testRender':
                 yield [$namelessOption, ['foo']];
+
                 yield [$namedOption, ['FOO', 'bar']];
+
                 yield [$numberedOption, ['FOO', 2, 'bar', 'foobar']];
+
                 yield [$emptyNamelessOption, []];
+
                 yield [$emptyNamedOption, []];
+
                 yield [$emptyNumberedOption, []];
+
                 yield [$trueFlagOption, ['FOO']];
+
                 yield [$falseFlagOption, []];
 
                 break;
 
             case 'testIsCompatible':
                 yield [$namelessOption, '1.0.0', true];
+
                 yield [$namelessOption, '1.5.0', true];
+
                 yield [$namelessOption, '2.0.0', true];
+
                 yield [$namelessOption, '2.5.0', true];
+
                 yield [$namelessOption, 'foo', false];
 
                 yield [$lowOptionOption, '1.0.0', true];
+
                 yield [$lowOptionOption, '1.5.0', true];
+
                 yield [$lowOptionOption, '2.0.0', true];
+
                 yield [$lowOptionOption, '2.5.0', true];
+
                 yield [$lowOptionOption, 'foo', false];
 
                 yield [$highOptionOption, '1.0.0', false];
+
                 yield [$highOptionOption, '1.5.0', false];
+
                 yield [$highOptionOption, '2.0.0', true];
+
                 yield [$highOptionOption, '2.5.0', true];
+
                 yield [$highOptionOption, 'foo', false];
 
                 break;
diff --git a/tests/Redis/Command/Option/NumberedOptionTest.php b/tests/Redis/Command/Option/NumberedOptionTest.php
index 4cde462..545c50a 100644
--- a/tests/Redis/Command/Option/NumberedOptionTest.php
+++ b/tests/Redis/Command/Option/NumberedOptionTest.php
@@ -70,24 +70,33 @@ public function dataProvider(string $testName): Generator
         switch ($testName) {
             case 'testGetOptionData':
                 yield [$noArgumentsOption, null];
+
                 yield [$emptyArgumentsOption, []];
+
                 yield [$withOneArgumentOption, ['bar']];
+
                 yield [$withMultipleArgumentOption, ['bar', 'foobar']];
 
                 break;
 
             case 'testIsValid':
                 yield [$noArgumentsOption, false];
+
                 yield [$emptyArgumentsOption, true];
+
                 yield [$withOneArgumentOption, true];
+
                 yield [$withMultipleArgumentOption, true];
 
                 break;
 
             case 'testRender':
                 yield [$noArgumentsOption, []];
+
                 yield [$emptyArgumentsOption, ['FOO', 0]];
+
                 yield [$withOneArgumentOption, ['FOO', 1, 'bar']];
+
                 yield [$withMultipleArgumentOption, ['FOO', 2, 'bar', 'foobar']];
 
                 break;
diff --git a/tests/Redis/InitializerTest.php b/tests/Redis/InitializerTest.php
index 7ebfdac..8cde0b2 100644
--- a/tests/Redis/InitializerTest.php
+++ b/tests/Redis/InitializerTest.php
@@ -51,11 +51,17 @@ public function dataProvider(string $testName): Generator
     {
         if ('testRediSearchVersion' === $testName) {
             yield [[['name', 'search', 'ver', '20005']], '2.0.5'];
+
             yield [[['name', 'search', 'ver', '20000']], '2.0.0'];
+
             yield [[['name', 'search', 'ver', '26512']], '2.65.12'];
+
             yield [[['name', 'search', 'ver', '120569']], '12.5.69'];
+
             yield [[['name', 'search', 'ver', '99999']], '9.99.99'];
+
             yield [[['name', 'json', 'ver', '99999']], null];
+
             yield [[['name', 'json', 'ver', '99999'], ['name', 'search', 'ver', '20005']], '2.0.5'];
         }
     }
diff --git a/tests/Redis/Response/InfoResponseTest.php b/tests/Redis/Response/InfoResponseTest.php
index 356e91a..c86f904 100644
--- a/tests/Redis/Response/InfoResponseTest.php
+++ b/tests/Redis/Response/InfoResponseTest.php
@@ -144,6 +144,7 @@ public function testResponse(): void
             ],
         ];
         $command = new Info();
+
         /** @var InfoResponse $parsed */
         $parsed = $command->parseResponse($rawResponse);
 
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 5175d4f..2d9234b 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -111,8 +111,10 @@ class DockerTest extends TestCase
 {
     /** @var bool */
     private static $skip = false;
+
     /** @var string */
     private static $containerCommand = 'podman';
+
     /** @var null|string */
     private static $containerId;
 
diff --git a/tests/psalm/stubs/phpiredis.php b/tests/psalm/stubs/phpiredis.php
index 57192c9..87cb532 100644
--- a/tests/psalm/stubs/phpiredis.php
+++ b/tests/psalm/stubs/phpiredis.php
@@ -39,6 +39,7 @@ function phpiredis_pconnect($host, $port = 6379): void
 function phpiredis_disconnect(): void
 {
 }
+
 /**
  * @param resource $redis
  * @param string[] $command
@@ -57,6 +58,7 @@ function phpiredis_command_bs($redis, array $command)
 function phpiredis_command($redis, string $command)
 {
 }
+
 /**
  * @param resource      $redis
  * @param array<string> $commands
@@ -113,6 +115,7 @@ function phpiredis_reader_feed($reader, string $response): void
 function phpiredis_reader_get_state($reader)
 {
 }
+
 /**
  * @param resource $reader
  *
@@ -144,6 +147,7 @@ function phpiredis_reader_destroy($redis): void
 function phpiredis_reader_set_error_handler($reader, ?callable $handler): void
 {
 }
+
 /**
  * @param resource $reader
  */

From aefc0f90f4381aac8e4e754c2513b74d66f2209e Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 5 Jun 2022 22:34:01 +0200
Subject: [PATCH 40/60] Allow Phpredis raw command to have 0 arguments

---
 CHANGELOG.md                        |  5 +++++
 src/Redis/Client/AmpRedisClient.php | 10 ++++++++--
 src/Redis/Client/PhpredisClient.php |  9 ++++-----
 3 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a03f621..92e44ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Fixed
+
+- Allow Phpredis raw command to have 0 arguments ([Issue#39])
+
 ### Changed
 
 - (dev) Update version and rules of PHP-CS-Fixer and PHPStan
@@ -196,6 +200,7 @@ First version
 [Issue#12]: https://github.com/MacFJA/php-redisearch/issues/12
 [Issue#16]: https://github.com/MacFJA/php-redisearch/issues/16
 [Issue#26]: https://github.com/MacFJA/php-redisearch/issues/26
+[Issue#39]: https://github.com/MacFJA/php-redisearch/issues/39
 [PR#1]: https://github.com/MacFJA/php-redisearch/pull/1
 [PR#3]: https://github.com/MacFJA/php-redisearch/pull/3
 [PR#8]: https://github.com/MacFJA/php-redisearch/pull/8
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
index 659fbce..de3df2e 100644
--- a/src/Redis/Client/AmpRedisClient.php
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\Redis\Client;
 
+use Amp\Promise;
 use function Amp\Promise\wait;
 use Amp\Redis\Redis;
 use function function_exists;
@@ -50,14 +51,19 @@ public static function make($redis): Client
 
     public function execute(Command $command)
     {
-        $result = wait($this->redis->query($command->getId(), ...array_map('strval', $command->getArguments())));
+        /** @var Promise<mixed> $query */
+        $query = $this->redis->query($command->getId(), ...array_map('strval', $command->getArguments()));
+        $result = wait($query);
 
         return $command->parseResponse($result);
     }
 
     public function executeRaw(...$args)
     {
-        return wait($this->redis->query(...array_map('strval', $args)));
+        /** @var Promise<mixed> $query */
+        $query = $this->redis->query(...array_map('strval', $args));
+
+        return wait($query);
     }
 
     public static function supports($redis): bool
diff --git a/src/Redis/Client/PhpredisClient.php b/src/Redis/Client/PhpredisClient.php
index fffb081..15194d3 100644
--- a/src/Redis/Client/PhpredisClient.php
+++ b/src/Redis/Client/PhpredisClient.php
@@ -49,9 +49,11 @@ public function execute(Command $command)
     {
         $arguments = $command->getArguments();
         if (0 === count($arguments)) {
-            $arguments = [null];
+            /** @psalm-suppress TooFewArguments */
+            $rawResponse = $this->redis->rawCommand($command->getId());
+        } else {
+            $rawResponse = $this->redis->rawCommand($command->getId(), ...$arguments);
         }
-        $rawResponse = $this->redis->rawCommand($command->getId(), ...$arguments);
 
         return $command->parseResponse($rawResponse);
     }
@@ -74,9 +76,6 @@ public function executeRaw(...$args)
         if (count($args) < 1) {
             return null;
         }
-        if (count($args) < 2) {
-            $args[] = null;
-        }
         // @phpstan-ignore-next-line
         return $this->redis->rawCommand(...$args);
     }

From efdf1e4383888df63ba729a778972feba956aec0 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Mon, 6 Jun 2022 14:16:04 +0200
Subject: [PATCH 41/60] Release 2.1.2

---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92e44ce..3dd63a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.1.2]
+
 ### Fixed
 
 - Allow Phpredis raw command to have 0 arguments ([Issue#39])
@@ -178,7 +180,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.1.1...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.1.2...HEAD
+[2.1.2]: https://github.com/MacFJA/php-redisearch/compare/2.1.1...2.1.2
 [2.1.1]: https://github.com/MacFJA/php-redisearch/compare/2.1.0...2.1.1
 [2.1.0]: https://github.com/MacFJA/php-redisearch/compare/2.0.2...2.1.0
 [2.0.2]: https://github.com/MacFJA/php-redisearch/compare/2.0.1...2.0.2

From 3f377e292c20925efc38675599268eafa41fa07d Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 27 Mar 2022 14:34:53 +0200
Subject: [PATCH 42/60] Improve paginated response

Rework paginated response (close #31)
Throw exception is client is not defined in Paginated response (close #30)
Increase the code coverage
---
 .gitignore                                    |   5 +-
 CHANGELOG.md                                  |  13 ++
 docs/BuildQuery.md                            |  27 +++
 docs/UsingResult.md                           |  70 +++++++
 src/Exception/MissingClientException.php      |  33 +++
 src/Redis/Client/AmpRedisClient.php           |   6 +
 src/Redis/Client/CheprasovRedisClient.php     |   3 +
 src/Redis/Client/CredisClient.php             |   3 +
 src/Redis/Client/PredisClient.php             |   3 +
 src/Redis/Client/RedisentClient.php           |   3 +
 .../Rediska/RediskaRediSearchCommand.php      |   3 +
 src/Redis/Client/RediskaClient.php            |   3 +
 src/Redis/Client/TinyRedisClient.php          |   3 +
 src/Redis/Response/AggregateResponseItem.php  |  18 +-
 src/Redis/Response/ArrayResponseTrait.php     |   7 +-
 src/Redis/Response/ClientAware.php            |  31 +++
 src/Redis/Response/ClientAwareTrait.php       |  42 ++++
 src/Redis/Response/CursorResponse.php         |  27 +--
 src/Redis/Response/PaginatedResponse.php      |  31 ++-
 src/Redis/Response/ResponseItem.php           |  37 ++++
 src/Redis/Response/SearchResponseItem.php     |   2 +-
 tests/Redis/Command/CursorReadTest.php        |  37 ++++
 tests/Redis/Command/IndexListTest.php         |   9 +
 .../Option/CustomValidatorOptionTest.php      |  19 ++
 .../Redis/Response/ArrayResponseTraitTest.php | 118 +++++++++++
 tests/Redis/Response/InfoResponseTest.php     |  54 +++++
 .../Redis/Response/PaginatedResponseTest.php  | 190 ++++++++++++++++++
 .../Redis/Command/FakePaginatedCommand.php    |  62 ++++++
 .../Response/ArrayResponseTraitTester.php     |  69 +++++++
 tests/integration/DockerTest.php              |  17 +-
 30 files changed, 900 insertions(+), 45 deletions(-)
 create mode 100644 docs/BuildQuery.md
 create mode 100644 docs/UsingResult.md
 create mode 100644 src/Exception/MissingClientException.php
 create mode 100644 src/Redis/Response/ClientAware.php
 create mode 100644 src/Redis/Response/ClientAwareTrait.php
 create mode 100644 src/Redis/Response/ResponseItem.php
 create mode 100644 tests/Redis/Response/ArrayResponseTraitTest.php
 create mode 100644 tests/Redis/Response/PaginatedResponseTest.php
 create mode 100644 tests/fixtures/Redis/Command/FakePaginatedCommand.php
 create mode 100644 tests/fixtures/Redis/Response/ArrayResponseTraitTester.php

diff --git a/.gitignore b/.gitignore
index 7e3383a..48607c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,8 +9,5 @@
 ### PHPUnit
 /.phpunit.result.cache
 
-### Documentation
-/docs
-
 ### Local test
-/test*.php
\ No newline at end of file
+/test*.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3dd63a7..6ee8c83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Added
+
+- Documentation on paginated responses
+- Throw custom exception is client is missing
+
+### Changed
+
+- Rework paginated responses
+
+### Deprecated
+
+- `\MacFJA\RediSearch\Redis\Response\AggregateResponseItem::getValue` (use `\MacFJA\RediSearch\Redis\Response\AggregateResponseItem::getFieldValue` instead)
+
 ## [2.1.2]
 
 ### Fixed
diff --git a/docs/BuildQuery.md b/docs/BuildQuery.md
new file mode 100644
index 0000000..f525e59
--- /dev/null
+++ b/docs/BuildQuery.md
@@ -0,0 +1,27 @@
+# Building a Query
+
+To help create RediSearch, the library offer a builder.
+
+It all start with the class `\MacFJA\RediSearch\Query\Builder`. From there you can add groups and query elements.
+
+---
+
+For example, for search all people named `John`, and preferably with the last name `Doe` that work as an accountant in the 10 km around Washington DC, and the resume contain text `cake` the request will be:
+```php
+use MacFJA\RediSearch\Query\Builder;
+use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use MacFJA\RediSearch\Query\Builder\TextFacet;
+use MacFJA\RediSearch\Query\Builder\Optional;
+use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+
+$queryBuilder = new Builder();
+$query = $queryBuilder
+    ->addTextFacet('firstname', 'John') // people named `John`
+    ->addElement(new Optional(new TextFacet(['lastname'], 'Doe'))) // preferably with the last name `Doe`
+    ->addTextFacet('job', 'accountant') // work as an accountant
+    ->addGeoFacet('address', -77.0365427, 38.8950368, 10, GeoFilterOption::UNIT_KILOMETERS) // in the 10 km around Washington DC
+    ->addString('cake') // the resume contain text `cake`
+    ->render();
+
+// @firstname:John ~@lastname:Doe @job:accountant @address:[-77.0365427 38.8950368 10 km] cake
+```
diff --git a/docs/UsingResult.md b/docs/UsingResult.md
new file mode 100644
index 0000000..e590606
--- /dev/null
+++ b/docs/UsingResult.md
@@ -0,0 +1,70 @@
+# Using result
+
+As you can set an offset and limit on `FT.SEARCH` and `FT.AGGREGATE`, they are paginated commands.
+The result of those commands is a page, that can contain one or more document.
+
+---
+
+So in this library, the result of `\MacFJA\RediSearch\Redis\Command\Search` and `\MacFJA\RediSearch\Redis\Command\Aggregate` is also a paginated object:
+either a `\MacFJA\RediSearch\Redis\Response\PaginatedResponse` (for Search and Aggregate) or a `\MacFJA\RediSearch\Redis\Response\CursorResponse` (for Aggregate)
+
+The 2 classes are representing a page.
+
+Both `\MacFJA\RediSearch\Redis\Response\PaginatedResponse` and `\MacFJA\RediSearch\Redis\Response\CursorResponse` can be read inside a `foreach` loop or with any iterator function.
+(As they implement the [`\Iterator` interface](https://www.php.net/manual/en/class.iterator.php))
+What you are iterating over, are the pages (not the documents inside the page).
+
+```php
+// If your RediSearch search have 15 results in total, but you set the pagination to 10 per page:
+/** @var PaginatedResponse $results */
+
+/** @var array<SearchResponseItem> $items */
+$items = $results->current();
+// $items is a list of 10 SearchResponseItem
+
+$results->next(); // To be able to class `->next()`, you need to call `->setClient()` first !
+
+/** @var array<SearchResponseItem> $items */
+$items = $results->current();
+// $items is a list of 5 SearchResponseItem
+```
+
+## Get all items on all pages
+
+```php
+/** @var \MacFJA\RediSearch\Redis\Client $client */
+/** @var \MacFJA\RediSearch\Redis\Command\Search $search */
+
+/** @var \MacFJA\RediSearch\Redis\Response\PaginatedResponse $results */
+$results = $client->execute($search);
+$results->setClient($client);
+
+$items = [];
+foreach ($results as $pageIndex => $pageContent) {
+    $items = array_merge($items, $pageContent);
+}
+/** @var \MacFJA\RediSearch\Redis\Response\SearchResponseItem $item */
+foreach ($items as $item) {
+    doSomething($item);
+}
+```
+Be careful, this will load all items in the memory.
+This will also call Redis multiple time if there are several pages.
+
+---
+
+If you need to avoid loading all items in the memory, you can use [`ResponseItemIterator`](https://github.com/MacFJA/php-redisearch-integration/blob/main/src/Iterator/ResponseItemIterator.php) from the sister project [macfja/redisearch-integration](https://github.com/MacFJA/php-redisearch-integration).
+This class it a custom iterator that will load in memory only one page at the time.
+
+```php
+/** @var \MacFJA\RediSearch\Redis\Client $client */
+/** @var \MacFJA\RediSearch\Redis\Command\Search $search */
+
+$results = $client->execute($search);
+$allItems = new \MacFJA\RediSearch\Integration\Iterator\ResponseItemIterator($results, $client);
+/** @var \MacFJA\RediSearch\Redis\Response\SearchResponseItem $item */
+foreach ($allItems as $item) {
+    // Only one page exist in the memory
+    doSomething($item);
+}
+```
diff --git a/src/Exception/MissingClientException.php b/src/Exception/MissingClientException.php
new file mode 100644
index 0000000..498cbd1
--- /dev/null
+++ b/src/Exception/MissingClientException.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Exception;
+
+use RuntimeException;
+use Throwable;
+
+class MissingClientException extends RuntimeException
+{
+    public function __construct(Throwable $previous = null)
+    {
+        parent::__construct('The Redis client is missing. You need to manually set it.', 0, $previous);
+    }
+}
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
index de3df2e..f94d878 100644
--- a/src/Redis/Client/AmpRedisClient.php
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -34,6 +34,9 @@ class AmpRedisClient extends AbstractClient
     /** @var Redis */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     private function __construct(Redis $redis)
     {
         if (!static::supports($redis)) {
@@ -83,6 +86,9 @@ public function pipeline(Command ...$commands): array
         }, $commands);
     }
 
+    /**
+     * @codeCoverageIgnore
+     */
     protected function doPipeline(Command ...$commands): array
     {
         return [];
diff --git a/src/Redis/Client/CheprasovRedisClient.php b/src/Redis/Client/CheprasovRedisClient.php
index 60d11c8..ca71cde 100644
--- a/src/Redis/Client/CheprasovRedisClient.php
+++ b/src/Redis/Client/CheprasovRedisClient.php
@@ -33,6 +33,9 @@ class CheprasovRedisClient extends AbstractClient
     /** @var AbstractRedisClient */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     private function __construct(AbstractRedisClient $redis)
     {
         if (!static::supports($redis)) {
diff --git a/src/Redis/Client/CredisClient.php b/src/Redis/Client/CredisClient.php
index f7fd35a..05c2cde 100644
--- a/src/Redis/Client/CredisClient.php
+++ b/src/Redis/Client/CredisClient.php
@@ -32,6 +32,9 @@ class CredisClient extends AbstractClient
     /** @var Credis_Client */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     private function __construct(Credis_Client $redis)
     {
         if (!self::supports($redis)) {
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
index aca9e71..efb108a 100644
--- a/src/Redis/Client/PredisClient.php
+++ b/src/Redis/Client/PredisClient.php
@@ -34,6 +34,9 @@ class PredisClient extends AbstractClient
     /** @var ClientInterface */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     private function __construct(ClientInterface $redis)
     {
         if (!static::supports($redis)) {
diff --git a/src/Redis/Client/RedisentClient.php b/src/Redis/Client/RedisentClient.php
index 5adc2a4..9a048e3 100644
--- a/src/Redis/Client/RedisentClient.php
+++ b/src/Redis/Client/RedisentClient.php
@@ -31,6 +31,9 @@ class RedisentClient extends AbstractClient
     /** @var Redis */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     private function __construct(Redis $redis)
     {
         if (!static::supports($redis)) {
diff --git a/src/Redis/Client/Rediska/RediskaRediSearchCommand.php b/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
index bda7822..9abadf6 100644
--- a/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
+++ b/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
@@ -26,6 +26,9 @@
 use Rediska_Command_Abstract;
 use Rediska_Connection_Exec;
 
+/**
+ * @codeCoverageIgnore
+ */
 class RediskaRediSearchCommand extends Rediska_Command_Abstract
 {
     public function create(): Rediska_Connection_Exec
diff --git a/src/Redis/Client/RediskaClient.php b/src/Redis/Client/RediskaClient.php
index b885e4a..aa7ae31 100644
--- a/src/Redis/Client/RediskaClient.php
+++ b/src/Redis/Client/RediskaClient.php
@@ -36,6 +36,9 @@ class RediskaClient extends AbstractClient
     /** @var Rediska */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     private function __construct(Rediska $redis)
     {
         if (!static::supports($redis)) {
diff --git a/src/Redis/Client/TinyRedisClient.php b/src/Redis/Client/TinyRedisClient.php
index 1055783..67c7a69 100644
--- a/src/Redis/Client/TinyRedisClient.php
+++ b/src/Redis/Client/TinyRedisClient.php
@@ -30,6 +30,9 @@ class TinyRedisClient extends AbstractClient
     /** @var \TinyRedisClient */
     private $redis;
 
+    /**
+     * @codeCoverageIgnore
+     */
     public function __construct(\TinyRedisClient $redis)
     {
         if (!self::supports($redis)) {
diff --git a/src/Redis/Response/AggregateResponseItem.php b/src/Redis/Response/AggregateResponseItem.php
index 96c9f39..1f6dbed 100644
--- a/src/Redis/Response/AggregateResponseItem.php
+++ b/src/Redis/Response/AggregateResponseItem.php
@@ -24,7 +24,7 @@
 /**
  * @codeCoverageIgnore Simple value object
  */
-class AggregateResponseItem
+class AggregateResponseItem implements ResponseItem
 {
     /** @var array<string,null|array<mixed>|float|int|string> */
     private $fields = [];
@@ -49,8 +49,24 @@ public function getFields(): array
      * @param null|array<mixed>|float|int|string $default
      *
      * @return null|array<mixed>|float|int|string
+     *
+     * @deprecated Use ::getFieldValue instead
+     * @codeCoverageIgnore
+     * @psalm-suppress
+     * @SuppressWarnings
      */
     public function getValue(string $fieldName, $default = null)
+    {
+        trigger_error(sprintf(
+            'Method %s is deprecated from version 2.2.0, use %s::getFieldValue instead',
+            __METHOD__,
+            __CLASS__
+        ), E_USER_DEPRECATED);
+        // @phpstan-ignore-next-line
+        return $this->getFieldValue($fieldName, $default);
+    }
+
+    public function getFieldValue(string $fieldName, $default = null)
     {
         return $this->fields[$fieldName] ?? $default;
     }
diff --git a/src/Redis/Response/ArrayResponseTrait.php b/src/Redis/Response/ArrayResponseTrait.php
index ed6f916..d5d1a20 100644
--- a/src/Redis/Response/ArrayResponseTrait.php
+++ b/src/Redis/Response/ArrayResponseTrait.php
@@ -86,9 +86,12 @@ private static function getNextValue(array $notKeyedArray, string $key)
      */
     private static function getKeys(array $notKeyedArray): array
     {
-        $keys = array_filter(array_values($notKeyedArray), static function (int $key): bool {
+        if (count($notKeyedArray) % 2 > 0) {
+            array_pop($notKeyedArray);
+        }
+        $keys = array_values(array_filter(array_values($notKeyedArray), static function (int $key): bool {
             return 0 === $key % 2;
-        }, ARRAY_FILTER_USE_KEY);
+        }, ARRAY_FILTER_USE_KEY));
 
         return array_map(static function ($key): string {
             return (string) $key;
diff --git a/src/Redis/Response/ClientAware.php b/src/Redis/Response/ClientAware.php
new file mode 100644
index 0000000..d221e52
--- /dev/null
+++ b/src/Redis/Response/ClientAware.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+use MacFJA\RediSearch\Redis\Client;
+
+interface ClientAware
+{
+    public function setClient(Client $client): ClientAware;
+
+    public function getClient(): ?Client;
+}
diff --git a/src/Redis/Response/ClientAwareTrait.php b/src/Redis/Response/ClientAwareTrait.php
new file mode 100644
index 0000000..3bbd51d
--- /dev/null
+++ b/src/Redis/Response/ClientAwareTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+use MacFJA\RediSearch\Redis\Client;
+
+trait ClientAwareTrait
+{
+    /** @var null|Client */
+    private $client;
+
+    public function getClient(): ?Client
+    {
+        return $this->client;
+    }
+
+    public function setClient(Client $client): ClientAware
+    {
+        $this->client = $client;
+
+        return $this;
+    }
+}
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index a477a46..7bbdc26 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -22,7 +22,9 @@
 namespace MacFJA\RediSearch\Redis\Response;
 
 use function count;
+use Countable;
 use Iterator;
+use MacFJA\RediSearch\Exception\MissingClientException;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\CursorRead;
@@ -31,17 +33,16 @@
 /**
  * @implements Iterator<int,AggregateResponseItem[]>
  */
-class CursorResponse implements Response, Iterator
+class CursorResponse implements Response, Iterator, Countable, ClientAware
 {
+    use ClientAwareTrait;
+
     /** @var array<AggregateResponseItem> */
     private $items;
 
     /** @var int */
     private $totalCount;
 
-    /** @var Client */
-    private $client;
-
     /** @var bool */
     private $doNext = false;
 
@@ -73,26 +74,23 @@ public function __construct(int $cursorId, int $totalCount, array $items, int $s
         $this->cursorId = $cursorId;
     }
 
-    public function setClient(Client $client): self
-    {
-        $this->client = $client;
-
-        return $this;
-    }
-
     /**
      * @return array<AggregateResponseItem>
      */
     public function current()
     {
         if ($this->doNext) {
+            if (!($this->getClient() instanceof Client)) {
+                throw new MissingClientException();
+            }
+
             $cursorRead = new CursorRead($this->redisVersion);
             $cursorRead->setIndex($this->index);
             $cursorRead->setCursorId($this->cursorId);
             $cursorRead->setCount($this->getPageSize());
 
             /** @var CursorResponse $next */
-            $next = $this->client->execute($cursorRead);
+            $next = $this->getClient()->execute($cursorRead);
             $this->cursorId = $next->cursorId;
             $this->offset += count($this->items);
             $this->items = $next->items;
@@ -145,4 +143,9 @@ public function getTotalCount(): int
     {
         return $this->totalCount;
     }
+
+    public function count()
+    {
+        return $this->getPageCount();
+    }
 }
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index d139526..945c8dc 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -25,16 +25,19 @@
 use Countable;
 use function is_int;
 use Iterator;
+use MacFJA\RediSearch\Exception\MissingClientException;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\PaginatedCommand;
 use MacFJA\RediSearch\Redis\Response;
 
 /**
- * @implements Iterator<int,AggregateResponseItem[]|SearchResponseItem[]>
+ * @implements Iterator<int,ResponseItem[]|AggregateResponseItem[]|SearchResponseItem[]>
  */
-class PaginatedResponse implements Response, Iterator, Countable
+class PaginatedResponse implements Response, Iterator, Countable, ClientAware
 {
-    /** @var array<AggregateResponseItem>|array<SearchResponseItem> */
+    use ClientAwareTrait;
+
+    /** @var array<ResponseItem> */
     private $items;
 
     /** @var PaginatedCommand */
@@ -43,9 +46,6 @@ class PaginatedResponse implements Response, Iterator, Countable
     /** @var int */
     private $totalCount;
 
-    /** @var Client */
-    private $client;
-
     /** @var null|int */
     private $requestedOffset;
 
@@ -53,7 +53,7 @@ class PaginatedResponse implements Response, Iterator, Countable
     private $requestedSize;
 
     /**
-     * @param AggregateResponseItem[]|SearchResponseItem[] $items
+     * @param ResponseItem[] $items
      */
     public function __construct(PaginatedCommand $command, int $totalCount, array $items)
     {
@@ -62,15 +62,8 @@ public function __construct(PaginatedCommand $command, int $totalCount, array $i
         $this->lastCommand = $command;
     }
 
-    public function setClient(Client $client): PaginatedResponse
-    {
-        $this->client = $client;
-
-        return $this;
-    }
-
     /**
-     * @return array<AggregateResponseItem>|array<SearchResponseItem>
+     * @return array<ResponseItem>
      */
     public function current()
     {
@@ -141,17 +134,21 @@ public function getTotalCount(): int
 
     public function count()
     {
-        return $this->getTotalCount();
+        return $this->getPageCount();
     }
 
     private function updateWithLimit(int $offset, int $size): void
     {
+        if (!($this->getClient() instanceof Client)) {
+            throw new MissingClientException();
+        }
+
         /** @var PaginatedCommand $nextCommand */
         $nextCommand = clone $this->lastCommand;
         $nextCommand->setLimit($offset, $size);
 
         /** @var PaginatedResponse $paginated */
-        $paginated = $this->client->execute($nextCommand);
+        $paginated = $this->getClient()->execute($nextCommand);
         $this->lastCommand = $nextCommand;
         $this->totalCount = $paginated->totalCount;
         $this->items = $paginated->items;
diff --git a/src/Redis/Response/ResponseItem.php b/src/Redis/Response/ResponseItem.php
new file mode 100644
index 0000000..a8f234d
--- /dev/null
+++ b/src/Redis/Response/ResponseItem.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Response;
+
+interface ResponseItem
+{
+    /**
+     * @return array<string,null|array<mixed>|float|int|string>
+     */
+    public function getFields(): array;
+
+    /**
+     * @param null|array<mixed>|bool|float|int|string $default
+     *
+     * @return null|array<mixed>|bool|float|int|string
+     */
+    public function getFieldValue(string $fieldName, $default = null);
+}
diff --git a/src/Redis/Response/SearchResponseItem.php b/src/Redis/Response/SearchResponseItem.php
index 0db7f35..6b1fa27 100644
--- a/src/Redis/Response/SearchResponseItem.php
+++ b/src/Redis/Response/SearchResponseItem.php
@@ -24,7 +24,7 @@
 /**
  * @codeCoverageIgnore Simple value object
  */
-class SearchResponseItem
+class SearchResponseItem implements ResponseItem
 {
     /** @var string */
     private $hash;
diff --git a/tests/Redis/Command/CursorReadTest.php b/tests/Redis/Command/CursorReadTest.php
index da7c0fd..b2b1e18 100644
--- a/tests/Redis/Command/CursorReadTest.php
+++ b/tests/Redis/Command/CursorReadTest.php
@@ -21,7 +21,9 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\CursorRead;
+use MacFJA\RediSearch\Redis\Response\CursorResponse;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -32,6 +34,8 @@
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Response\CursorResponse
+ * @uses \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
  *
  * @internal
  */
@@ -63,4 +67,37 @@ public function testFullOption(): void
         $command->setCount(30);
         static::assertSame(['READ', 'idx', 398, 'COUNT', 30], $command->getArguments());
     }
+
+    public function testParseResponseWithError(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+
+        $command = new CursorRead();
+        $command->parseResponse('');
+    }
+
+    public function testParseResponse(): void
+    {
+        $rawResponse = [
+            0 => [
+                0 => 0,
+                1 => [
+                    0 => 'f1',
+                    1 => 'bar',
+                ],
+                2 => [
+                    0 => 'f1',
+                    1 => 'baz',
+                ],
+            ],
+            1 => 976015094,
+        ];
+        $command = new CursorRead();
+        $command->setIndex('idx')
+            ->setCursorId(976015094)
+        ;
+        $response = $command->parseResponse($rawResponse);
+
+        static::assertInstanceOf(CursorResponse::class, $response);
+    }
 }
diff --git a/tests/Redis/Command/IndexListTest.php b/tests/Redis/Command/IndexListTest.php
index 97b5737..0504d1d 100644
--- a/tests/Redis/Command/IndexListTest.php
+++ b/tests/Redis/Command/IndexListTest.php
@@ -36,4 +36,13 @@ public function testGetId(): void
         $command = new IndexList();
         static::assertSame('FT._LIST', $command->getId());
     }
+
+    public function testGetRediSearchVersion(): void
+    {
+        $command = new IndexList();
+
+        static::assertSame('2.0.0', $command->getRediSearchVersion());
+        $command->setRediSearchVersion('2.2.0');
+        static::assertSame('2.2.0', $command->getRediSearchVersion());
+    }
 }
diff --git a/tests/Redis/Command/Option/CustomValidatorOptionTest.php b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
index 925d40d..0d2df9f 100644
--- a/tests/Redis/Command/Option/CustomValidatorOptionTest.php
+++ b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command\Option;
 
+use BadMethodCallException;
 use Generator;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
@@ -61,6 +62,24 @@ public function testShorthands(): void
         static::assertFalse($invalidList->isValid());
     }
 
+    public function testInvalidMethodName(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('Call undefined method invalidName');
+
+        $decorator = new CustomValidatorOption(new NamelessOption('foo'), new Length(4));
+        // @phpstan-ignore-next-line
+        $decorator->invalidName();
+    }
+
+    public function testVersionConstraint(): void
+    {
+        $decorator = new CustomValidatorOption(new NamelessOption('foo'), new Length(4));
+        static::assertSame('*', $decorator->getVersionConstraint());
+        $decorator = new CustomValidatorOption(new NamelessOption('foo', '>=1.0.0'), new Length(4));
+        static::assertSame('>=1.0.0', $decorator->getVersionConstraint());
+    }
+
     public function dataProvider(string $testName): Generator
     {
         $invalidMinLengthOption = new CustomValidatorOption(new NamelessOption('foo'), new Length(4));
diff --git a/tests/Redis/Response/ArrayResponseTraitTest.php b/tests/Redis/Response/ArrayResponseTraitTest.php
new file mode 100644
index 0000000..19bd0df
--- /dev/null
+++ b/tests/Redis/Response/ArrayResponseTraitTest.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Response;
+
+use MacFJA\RediSearch\tests\fixtures\Redis\Response\ArrayResponseTraitTester;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Response\ArrayResponseTrait
+ *
+ * @internal
+ */
+class ArrayResponseTraitTest extends TestCase
+{
+    public function testOddGetPairs(): void
+    {
+        $data = [1, 2, 3];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(['1' => 2], $inlineArrayResponse->publicGetPairs($data));
+    }
+
+    public function testEvenGetPairs(): void
+    {
+        $data = [1, 2, 3, 4];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(['1' => 2, '3' => 4], $inlineArrayResponse->publicGetPairs($data));
+    }
+
+    public function testOddGetKeys(): void
+    {
+        $data = [1, 2, 3];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(['1'], $inlineArrayResponse->publicGetKeys($data));
+    }
+
+    public function testEvenGetKeys(): void
+    {
+        $data = [1, 2, 3, 4];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(['1', '3'], $inlineArrayResponse->publicGetKeys($data));
+    }
+
+    public function testOddGetValue(): void
+    {
+        $data = [1, 2, 3];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(2, $inlineArrayResponse->publicGetValue($data, '1'));
+        static::assertNull($inlineArrayResponse->publicGetValue($data, '3'));
+    }
+
+    public function testEvenGetValue(): void
+    {
+        $data = [1, 2, 3, 4];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(2, $inlineArrayResponse->publicGetValue($data, '1'));
+        static::assertSame(4, $inlineArrayResponse->publicGetValue($data, '3'));
+        static::assertNull($inlineArrayResponse->publicGetValue($data, '6'));
+    }
+
+    public function testOddGetNextValue(): void
+    {
+        $data = [1, 2, 3];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(2, $inlineArrayResponse->publicGetNextValue($data, '1'));
+        static::assertSame(3, $inlineArrayResponse->publicGetNextValue($data, '2'));
+        static::assertNull($inlineArrayResponse->publicGetNextValue($data, '3'));
+    }
+
+    public function testEvenGetNextValue(): void
+    {
+        $data = [1, 2, 3, 4];
+
+        $inlineArrayResponse = $this->getInlineArrayResponse();
+
+        static::assertSame(2, $inlineArrayResponse->publicGetNextValue($data, '1'));
+        static::assertSame(3, $inlineArrayResponse->publicGetNextValue($data, '2'));
+        static::assertSame(4, $inlineArrayResponse->publicGetNextValue($data, '3'));
+        static::assertNull($inlineArrayResponse->publicGetNextValue($data, '6'));
+    }
+
+    private function getInlineArrayResponse(): ArrayResponseTraitTester
+    {
+        return new ArrayResponseTraitTester();
+    }
+}
diff --git a/tests/Redis/Response/InfoResponseTest.php b/tests/Redis/Response/InfoResponseTest.php
index c86f904..747bec8 100644
--- a/tests/Redis/Response/InfoResponseTest.php
+++ b/tests/Redis/Response/InfoResponseTest.php
@@ -23,6 +23,10 @@
 
 use BadMethodCallException;
 use InvalidArgumentException;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
 use MacFJA\RediSearch\Redis\Command\Info;
 use MacFJA\RediSearch\Redis\Response\InfoResponse;
 use PHPUnit\Framework\TestCase;
@@ -36,6 +40,9 @@
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand::__construct
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
@@ -79,6 +86,22 @@ public function testResponse(): void
                     3 => new Status('WEIGHT'),
                     4 => '1',
                 ],
+                1 => [
+                    0 => new Status('labels'),
+                    1 => new Status('type'),
+                    2 => new Status('TAG'),
+                ],
+                2 => [
+                    0 => new Status('place'),
+                    1 => new Status('type'),
+                    2 => new Status('GEO'),
+                ],
+                3 => [
+                    0 => new Status('count'),
+                    1 => new Status('type'),
+                    2 => new Status('NUMERIC'),
+                    3 => new Status('NOINDEX'),
+                ],
             ],
             8 => new Status('num_docs'),
             9 => '4',
@@ -176,6 +199,13 @@ public function doTestGetFieldsAsOption(InfoResponse $parsed): void
         $response = $parsed->getFieldsAsOption();
 
         static::assertSame(['text', 'TEXT', 'WEIGHT', '1'], $response[0]->render());
+        static::assertInstanceOf(TextFieldOption::class, $response[0]);
+        static::assertSame(['labels', 'TAG'], $response[1]->render());
+        static::assertInstanceOf(TagFieldOption::class, $response[1]);
+        static::assertSame(['place', 'GEO'], $response[2]->render());
+        static::assertInstanceOf(GeoFieldOption::class, $response[2]);
+        static::assertSame(['count', 'NUMERIC', 'NOINDEX'], $response[3]->render());
+        static::assertInstanceOf(NumericFieldOption::class, $response[3]);
     }
 
     public function doTestGetIndexDefinition(InfoResponse $parsed): void
@@ -232,4 +262,28 @@ public function testNotANumber(): void
 
         static::assertNull($response->getNumDocs());
     }
+
+    public function testDataCasting(): void
+    {
+        $response = new InfoResponse([
+            'records_per_doc_avg', '1.5',
+            'max_doc_id', '50',
+            'index_name', 'idx',
+            'indexing', '1',
+            'non_existent_string', 'hello',
+            'non_existent_float', 1.2,
+            'non_existent_int', 1,
+        ]);
+
+        static::assertSame(1.5, $response->getRecordsPerDocAvg());
+        static::assertSame(50, $response->getMaxDocId());
+        static::assertSame('idx', $response->getIndexName());
+        static::assertTrue($response->getIndexing());
+        // @phpstan-ignore-next-line
+        static::assertSame('hello', $response->getNonExistentString());
+        // @phpstan-ignore-next-line
+        static::assertSame(1.2, $response->getNonExistentFloat());
+        // @phpstan-ignore-next-line
+        static::assertSame(1, $response->getNonExistentInt());
+    }
 }
diff --git a/tests/Redis/Response/PaginatedResponseTest.php b/tests/Redis/Response/PaginatedResponseTest.php
new file mode 100644
index 0000000..d85ac3e
--- /dev/null
+++ b/tests/Redis/Response/PaginatedResponseTest.php
@@ -0,0 +1,190 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Response;
+
+use function array_slice;
+use MacFJA\RediSearch\Exception\MissingClientException;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command\PaginatedCommand;
+use MacFJA\RediSearch\Redis\Response\PaginatedResponse;
+use MacFJA\RediSearch\Redis\Response\SearchResponseItem;
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakePaginatedCommand;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Exception\MissingClientException
+ * @covers \MacFJA\RediSearch\Redis\Response\PaginatedResponse
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @internal
+ */
+class PaginatedResponseTest extends TestCase
+{
+    public function testMultiplePages(): void
+    {
+        [$client, $command] = $this->getClientAndCommand();
+
+        /** @var PaginatedResponse $response */
+        $response = $client->execute($command);
+
+        static::assertInstanceOf(PaginatedResponse::class, $response);
+
+        static::assertTrue($response->valid());
+        static::assertSame('foo', $response->current()[0]->getFieldValue('f1'));
+        static::assertCount(4, $response);
+        static::assertEquals(2, $response->getPageSize());
+        static::assertEquals(4, $response->getPageCount());
+        static::assertEquals(7, $response->getTotalCount());
+        static::assertEquals(0, $response->key());
+
+        $response->setClient($client);
+
+        $response->next();
+
+        static::assertTrue($response->valid());
+        static::assertSame('bob', $response->current()[0]->getFieldValue('f1'));
+        static::assertCount(4, $response);
+        static::assertEquals(2, $response->getPageSize());
+        static::assertEquals(4, $response->getPageCount());
+        static::assertEquals(7, $response->getTotalCount());
+        static::assertEquals(1, $response->key());
+
+        $response->next();
+
+        static::assertTrue($response->valid());
+        static::assertSame('lorem', $response->current()[0]->getFieldValue('f1'));
+        static::assertCount(4, $response);
+        static::assertEquals(2, $response->getPageSize());
+        static::assertEquals(4, $response->getPageCount());
+        static::assertEquals(7, $response->getTotalCount());
+        static::assertEquals(2, $response->key());
+
+        $response->next();
+
+        static::assertTrue($response->valid());
+        static::assertSame('odd', $response->current()[0]->getFieldValue('f1'));
+        static::assertCount(4, $response);
+        static::assertEquals(2, $response->getPageSize());
+        static::assertEquals(4, $response->getPageCount());
+        static::assertEquals(7, $response->getTotalCount());
+        static::assertEquals(3, $response->key());
+
+        $response->next();
+
+        static::assertFalse($response->valid());
+    }
+
+    public function testMultiplePagesIterator(): void
+    {
+        [$client, $command] = $this->getClientAndCommand();
+
+        /** @var PaginatedResponse $response */
+        $response = $client->execute($command);
+
+        static::assertInstanceOf(PaginatedResponse::class, $response);
+        $response->setClient($client);
+
+        foreach ($response as $index => $page) {
+            if (0 === $index) {
+                static::assertSame('foo', $response->current()[0]->getFieldValue('f1'));
+            }
+            if (1 === $index) {
+                static::assertSame('bob', $response->current()[0]->getFieldValue('f1'));
+            }
+            if (2 === $index) {
+                static::assertSame('lorem', $response->current()[0]->getFieldValue('f1'));
+            }
+            if (3 === $index) {
+                static::assertSame('odd', $response->current()[0]->getFieldValue('f1'));
+            }
+
+            static::assertEquals(2, $response->getPageSize());
+            static::assertEquals(4, $response->getPageCount());
+            static::assertEquals(7, $response->getTotalCount());
+        }
+    }
+
+    public function testMissingClient(): void
+    {
+        $this->expectException(MissingClientException::class);
+        [$client, $command] = $this->getClientAndCommand();
+
+        /** @var PaginatedResponse $response */
+        $response = $client->execute($command);
+
+        static::assertInstanceOf(PaginatedResponse::class, $response);
+
+        static::assertTrue($response->valid());
+
+        $response->next();
+        $response->current();
+    }
+
+    public function testPageSizeSetToZero(): void
+    {
+        $command = new FakePaginatedCommand([]);
+        $command->setLimit(0, 0);
+        $response = new PaginatedResponse($command, 2, []);
+
+        static::assertSame(0, $response->key());
+        static::assertSame(0, $response->getPageCount());
+        static::assertCount(0, $response);
+    }
+
+    /**
+     * @return array{0:Client,1:FakePaginatedCommand}
+     */
+    private function getClientAndCommand(): array
+    {
+        $command = new FakePaginatedCommand([]);
+
+        $client = $this->createMock(Client::class);
+        $client->method('execute')->willReturnOnConsecutiveCalls(
+            self::getResponses($command, 0, 2),
+            self::getResponses($command, 2, 4),
+            self::getResponses($command, 4, 6),
+            self::getResponses($command, 6, 7)
+        );
+
+        return [$client, $command];
+    }
+
+    private static function getResponses(PaginatedCommand $command, int $from, int $to): PaginatedResponse
+    {
+        $response = [
+            new SearchResponseItem('foobar1', ['f1' => 'foo', 'f2' => 'bar']),
+            new SearchResponseItem('foobar2', ['f1' => 'baz', 'f2' => 'alice']),
+            new SearchResponseItem('foobar3', ['f1' => 'bob', 'f2' => 'charlie']),
+            new SearchResponseItem('foobar4', ['f1' => 'john', 'f2' => 'doe']),
+            new SearchResponseItem('foobar5', ['f1' => 'lorem', 'f2' => 'ipsum']),
+            new SearchResponseItem('foobar6', ['f1' => 'foobar', 'f2' => 'foobaz']),
+            new SearchResponseItem('foobar7', ['f1' => 'odd', 'f2' => 'page']),
+        ];
+
+        return new PaginatedResponse(
+            $command,
+            7,
+            array_slice($response, $from, $to - $from, false)
+        );
+    }
+}
diff --git a/tests/fixtures/Redis/Command/FakePaginatedCommand.php b/tests/fixtures/Redis/Command/FakePaginatedCommand.php
new file mode 100644
index 0000000..607ac74
--- /dev/null
+++ b/tests/fixtures/Redis/Command/FakePaginatedCommand.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command;
+use MacFJA\RediSearch\Redis\Command\PaginatedCommand;
+
+class FakePaginatedCommand extends Command\AbstractCommand implements PaginatedCommand
+{
+    /** @var null|int */
+    private $size;
+
+    /** @var null|int */
+    private $offset;
+
+    public function getId(): string
+    {
+        return 'fake';
+    }
+
+    public function getOffset(): ?int
+    {
+        return $this->offset;
+    }
+
+    public function getSize(): ?int
+    {
+        return $this->size;
+    }
+
+    public function setLimit(int $offset, int $size): PaginatedCommand
+    {
+        $this->offset = $offset;
+        $this->size = $size;
+
+        return $this;
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return [];
+    }
+}
diff --git a/tests/fixtures/Redis/Response/ArrayResponseTraitTester.php b/tests/fixtures/Redis/Response/ArrayResponseTraitTester.php
new file mode 100644
index 0000000..e6a9708
--- /dev/null
+++ b/tests/fixtures/Redis/Response/ArrayResponseTraitTester.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Response;
+
+use MacFJA\RediSearch\Redis\Response\ArrayResponseTrait;
+
+class ArrayResponseTraitTester
+{
+    use ArrayResponseTrait;
+
+    /**
+     * @param array<float|int|mixed|string> $notKeyedArray
+     *
+     * @return array<string,mixed>
+     */
+    public function publicGetPairs($notKeyedArray)
+    {
+        return self::getPairs($notKeyedArray);
+    }
+
+    /**
+     * @param array<float|int|mixed|string> $notKeyedArray
+     *
+     * @return array<string>
+     */
+    public function publicGetKeys($notKeyedArray)
+    {
+        return self::getKeys($notKeyedArray);
+    }
+
+    /**
+     * @param array<array|mixed> $notKeyedArray
+     *
+     * @return null|array<mixed>|bool|float|int|string
+     */
+    public function publicGetValue(array $notKeyedArray, string $key)
+    {
+        return self::getValue($notKeyedArray, $key);
+    }
+
+    /**
+     * @param array<array|mixed> $notKeyedArray
+     *
+     * @return null|bool|float|int|string
+     */
+    public function publicGetNextValue(array $notKeyedArray, string $key)
+    {
+        return self::getNextValue($notKeyedArray, $key);
+    }
+}
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 2d9234b..5426537 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -58,7 +58,6 @@
  * @covers \MacFJA\RediSearch\Redis\Client\CredisClient
  * @covers \MacFJA\RediSearch\Redis\Client\PredisClient
  * @covers \MacFJA\RediSearch\Redis\Client\RedisentClient
- * @covers \MacFJA\RediSearch\Redis\Client\Rediska\RediskaRediSearchCommand
  * @covers \MacFJA\RediSearch\Redis\Client\RediskaClient
  * @covers \MacFJA\RediSearch\Redis\Client\TinyRedisClient
  * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
@@ -214,8 +213,9 @@ public function testIntegration($clientBuilder): void
 
         /** @var PaginatedResponse $result */
         $result = $client->execute($search);
-        static::assertCount(3, $result);
+        static::assertCount(1, $result);
         static::assertSame(1, $result->getPageCount());
+        static::assertSame(3, $result->getTotalCount());
 
         /** @var array<SearchResponseItem> $page */
         foreach ($result as $page) {
@@ -255,12 +255,13 @@ public function testIntegration($clientBuilder): void
                 ->addGroupBy(new GroupByOption([], [ReduceOption::toList('age', 'list')]))
         );
 
-        static::assertCount(3, $result[0]);
-        static::assertEquals('1', current($result[1])[0]->getValue('count'));
-        static::assertEquals('2', current($result[1])[1]->getValue('count'));
-        static::assertEquals('3', current($result[2])[0]->getValue('count'));
-        static::assertEquals('3', current($result[3])[0]->getValue('count'));
-        static::assertEquals(['30'], current($result[4])[0]->getValue('list'));
+        static::assertCount(1, $result[0]);
+        static::assertSame(3, $result[0]->getTotalCount());
+        static::assertEquals('1', current($result[1])[0]->getFieldValue('count'));
+        static::assertEquals('2', current($result[1])[1]->getFieldValue('count'));
+        static::assertEquals('3', current($result[2])[0]->getFieldValue('count'));
+        static::assertEquals('3', current($result[3])[0]->getFieldValue('count'));
+        static::assertEquals(['30'], current($result[4])[0]->getFieldValue('list'));
 
         $client->execute((new DropIndex())->setIndex('testDoc')->setDeleteDocument());
         $client->executeRaw('del', ...$docToRemove);

From 88821febff030ba1f1e5b2081ec090b7b629c228 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 12 Jun 2022 23:29:46 +0200
Subject: [PATCH 43/60] Missing default values on highlight option

---
 CHANGELOG.md                 | 5 +++++
 src/Redis/Command/Search.php | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ee8c83..6e078bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Documentation on paginated responses
 - Throw custom exception is client is missing
 
+### Fixed
+
+- Missing default values on highlight option ([Issue#38])
+
 ### Changed
 
 - Rework paginated responses
@@ -216,6 +220,7 @@ First version
 [Issue#12]: https://github.com/MacFJA/php-redisearch/issues/12
 [Issue#16]: https://github.com/MacFJA/php-redisearch/issues/16
 [Issue#26]: https://github.com/MacFJA/php-redisearch/issues/26
+[Issue#38]: https://github.com/MacFJA/php-redisearch/issues/38
 [Issue#39]: https://github.com/MacFJA/php-redisearch/issues/39
 [PR#1]: https://github.com/MacFJA/php-redisearch/pull/1
 [PR#3]: https://github.com/MacFJA/php-redisearch/pull/3
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 97d0604..15008c5 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -161,7 +161,7 @@ public function setSummarize(?array $fields = null, ?int $fragmentCount = null,
      *
      * @return $this
      */
-    public function setHighlight(?array $fields, ?string $openTag, ?string $closeTag): self
+    public function setHighlight(?array $fields, ?string $openTag = null, ?string $closeTag = null): self
     {
         $this->options['highlight']
             ->setDataOfOption('type', true)

From 5d488b2b8a84217fcbb57630dcd3a3c6054ad7d4 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 5 Jul 2022 19:41:42 +0200
Subject: [PATCH 44/60] Add Predis v2 compatibility

---
 CHANGELOG.md                      |  4 ++++
 Makefile                          | 11 +++++++++--
 README.md                         |  4 ++--
 composer.json                     |  2 +-
 phpstan.neon                      |  3 ++-
 src/Redis/Client/PredisClient.php | 30 +++++++++++++++++++++++++++---
 src/Redis/Initializer.php         | 13 ++++++++++++-
 7 files changed, 57 insertions(+), 10 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e078bf..4f43fdc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Documentation on paginated responses
 - Throw custom exception is client is missing
+- Add compatibility for Predis version 2.x
+- (dev) Add `make clean` to remove all generated files
 
 ### Fixed
 
@@ -18,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 
 - Rework paginated responses
+- Add warning for Predis commands initialization
+- (dev) Remove deprecated `--no-suggest` option of Composer in the `Makefile`
 
 ### Deprecated
 
diff --git a/Makefile b/Makefile
index ac983c9..150e368 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: analyze fix-code test test-with-integration coverage coverage-with-integration validation integration-test integration-coverage
+.PHONY: analyze fix-code clean test test-with-integration coverage coverage-with-integration validation integration-test integration-coverage
 
 analyze: | vendor
 	$(COMPOSER) exec -v parallel-lint -- src
@@ -11,6 +11,13 @@ analyze: | vendor
 	$(COMPOSER) exec -v phpstan -- analyse
 	$(COMPOSER) exec -v psalm -- src
 
+clean:
+	rm -rf composer.phar
+	rm -rf vendor/
+	rm -rf composer.lock
+	rm -rf .php-cs-fixer.cache
+	rm -rf .phpunit.result.cache
+
 fix-code: | vendor
 	$(COMPOSER) normalize
 	$(COMPOSER) exec -v php-cs-fixer -- fix -v
@@ -39,7 +46,7 @@ integration-coverage: | vendor
 validation: fix-code analyze test-with-integration coverage-with-integration
 
 vendor: composer.json
-	$(COMPOSER) install --optimize-autoloader --no-suggest --prefer-dist
+	$(COMPOSER) install --optimize-autoloader --prefer-dist
 	touch vendor
 
 composer.phar:
diff --git a/README.md b/README.md
index a115603..38d8ac0 100644
--- a/README.md
+++ b/README.md
@@ -182,7 +182,7 @@ $result = $client->pipeline($search, $stats, $aggregate, $suggestion);
 // $result[3] is the suggestion result
 ```
 
-### Use Predis shorthand syntax
+### Use Predis (v1.x) shorthand syntax
 
 ```php
 $client = new \Predis\Client(/* ... */);
@@ -225,4 +225,4 @@ See [CONTRIBUTING](CONTRIBUTING.md) for more information.
 
 ## License
 
-The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
\ No newline at end of file
+The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
diff --git a/composer.json b/composer.json
index e6ca3be..145cdce 100644
--- a/composer.json
+++ b/composer.json
@@ -39,7 +39,7 @@
         "phpmd/phpmd": "^2.10",
         "phpstan/phpstan": "^1.2.0",
         "phpunit/phpunit": "^8.5 || ^9.3",
-        "predis/predis": "^1.1",
+        "predis/predis": "^1.1 || ^2.0",
         "ptrofimov/tinyredisclient": "^1.1",
         "redisent/redisent": "dev-master",
         "roave/security-advisories": "dev-latest",
diff --git a/phpstan.neon b/phpstan.neon
index 45f1cab..78d090b 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -13,4 +13,5 @@ parameters:
         - '#Method MacFJA\\RediSearch\\tests\\Redis.+::dataProvider\(\) return type has no value type specified in iterable type (array|Generator).#'
         -
             message: '#Call to an undefined method MacFJA\\RediSearch\\Redis\\Command\\Option\\CommandOption::set\w+\(\).#'
-            path: src/Redis/Command/Option/GroupedOption.php
\ No newline at end of file
+            path: src/Redis/Command/Option/GroupedOption.php
+        - '#Predis\\Profile\\RedisProfile#'
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
index efb108a..51ad1dd 100644
--- a/src/Redis/Client/PredisClient.php
+++ b/src/Redis/Client/PredisClient.php
@@ -26,7 +26,9 @@
 use MacFJA\RediSearch\Redis\Command;
 use Predis\ClientContextInterface;
 use Predis\ClientInterface;
+use Predis\Command\CommandInterface;
 use Predis\Command\RawCommand;
+use Predis\Command\RawFactory;
 use RuntimeException;
 
 class PredisClient extends AbstractClient
@@ -34,6 +36,9 @@ class PredisClient extends AbstractClient
     /** @var ClientInterface */
     private $redis;
 
+    /** @var null|RawFactory */
+    private static $rawFactory;
+
     /**
      * @codeCoverageIgnore
      */
@@ -51,7 +56,7 @@ private function __construct(ClientInterface $redis)
 
     public function execute(Command $command)
     {
-        $rawResponse = $this->redis->executeCommand(new RawCommand(array_merge([$command->getId()], $command->getArguments())));
+        $rawResponse = $this->redis->executeCommand(self::createRawCommand(array_merge([$command->getId()], $command->getArguments())));
 
         return $command->parseResponse($rawResponse);
     }
@@ -82,7 +87,7 @@ public static function make($redis): Client
      */
     public function executeRaw(...$args)
     {
-        return $this->redis->executeCommand(new RawCommand($args));
+        return $this->redis->executeCommand(self::createRawCommand($args));
     }
 
     protected function doPipeline(Command ...$commands): array
@@ -91,8 +96,27 @@ protected function doPipeline(Command ...$commands): array
 
         return $this->redis->pipeline(static function (ClientContextInterface $pipeline) use ($commands): void {
             foreach ($commands as $command) {
-                $pipeline->executeCommand(new RawCommand(array_merge([$command->getId()], $command->getArguments())));
+                $pipeline->executeCommand(self::createRawCommand(array_merge([$command->getId()], $command->getArguments())));
             }
         });
     }
+
+    /**
+     * @param array<float|int|string> $args
+     * @codeCoverageIgnore
+     */
+    private static function createRawCommand(array $args): CommandInterface
+    {
+        if (class_exists(RawFactory::class)) {
+            if (null === self::$rawFactory) {
+                self::$rawFactory = new RawFactory();
+            }
+            $commandID = array_shift($args);
+
+            return self::$rawFactory->create((string) $commandID, $args);
+        }
+
+        // @phpstan-ignore-next-line
+        return new RawCommand($args);
+    }
 }
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 6f988c8..865f3af 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\Redis;
 
+use MacFJA\RediSearch\Redis\Client\AbstractClient;
 use MacFJA\RediSearch\Redis\Client\Rediska\RediskaRediSearchCommand;
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
@@ -59,9 +60,19 @@ class Initializer
 {
     /**
      * @codeCoverageIgnore
+     * @psalm-suppress UndefinedDocblockClass
+     * @psalm-suppress UndefinedClass
+     *
+     * @param RedisProfile $profile
      */
-    public static function registerCommandsPredis(RedisProfile $profile): void
+    public static function registerCommandsPredis($profile): void
     {
+        if (!$profile instanceof RedisProfile) {
+            false === AbstractClient::$disableNotice
+            && trigger_error(sprintf('The parameter $profile must be an instance of %s, which is only available in Predis 1.x.', RedisProfile::class), E_USER_WARNING);
+
+            return;
+        }
         $profile->defineCommand('ftaggregate', Aggregate::class);
         $profile->defineCommand('ftaliasadd', AliasAdd::class);
         $profile->defineCommand('ftaliasdel', AliasDel::class);

From 165db385cd77850663b0fb4a8873f3cf71a932c4 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 24 Jul 2022 19:51:37 +0200
Subject: [PATCH 45/60] Update PHP-CS-Fixer version

---
 .php-cs-fixer.dist.php                          |  4 +++-
 CHANGELOG.md                                    |  1 +
 .../OutOfRangeLevenshteinDistanceException.php  |  3 ++-
 .../UnexpectedServerResponseException.php       |  1 +
 src/Exception/UnknownUnitException.php          |  1 +
 src/Index.php                                   |  1 +
 src/IndexBuilder.php                            |  7 ++++---
 src/Query/Builder/AndGroup.php                  |  2 +-
 src/Query/Builder/FieldFacet.php                |  3 ++-
 src/Query/Builder/GeoFacet.php                  |  1 +
 src/Query/Builder/NumericFacet.php              |  1 +
 src/Query/Builder/QueryElementAttribute.php     |  3 ++-
 src/Query/Builder/TagFacet.php                  |  1 +
 src/Query/Builder/TextFacet.php                 |  1 +
 src/Redis/Client/AbstractClient.php             |  3 ++-
 src/Redis/Client/AmpRedisClient.php             |  5 +++--
 src/Redis/Client/CredisClient.php               |  3 ++-
 src/Redis/Client/PhpiredisClient.php            |  3 ++-
 src/Redis/Client/PhpredisClient.php             |  1 +
 src/Redis/Client/PredisClient.php               |  1 +
 .../Client/Rediska/RediskaRediSearchCommand.php |  1 +
 src/Redis/Client/RediskaClient.php              |  1 +
 src/Redis/Command/AbstractCommand.php           |  3 ++-
 src/Redis/Command/AddFieldOptionTrait.php       |  3 ++-
 src/Redis/Command/Aggregate.php                 |  1 +
 .../Command/AggregateCommand/GroupByOption.php  |  1 +
 .../Command/AggregateCommand/ReduceOption.php   |  1 +
 .../Command/AggregateCommand/SortByOption.php   |  1 +
 src/Redis/Command/CursorRead.php                |  1 +
 src/Redis/Command/Info.php                      |  1 +
 .../Command/Option/AbstractCommandOption.php    |  3 ++-
 src/Redis/Command/Option/GroupedOption.php      |  3 ++-
 src/Redis/Command/Option/NotEmptyOption.php     |  5 ++++-
 .../Option/WithPublicGroupedSetterTrait.php     |  3 ++-
 src/Redis/Command/Search.php                    |  1 +
 src/Redis/Command/SearchCommand/LimitOption.php |  1 +
 src/Redis/Command/SpellCheck.php                |  1 +
 .../Command/SpellCheckCommand/TermsOption.php   |  1 +
 src/Redis/Command/SugGet.php                    |  1 +
 src/Redis/Response/CursorResponse.php           |  3 ++-
 src/Redis/Response/InfoResponse.php             |  5 +++--
 src/Redis/Response/PaginatedResponse.php        | 17 +++++++++--------
 src/Suggestions.php                             |  1 +
 tests/Query/BuilderTest.php                     |  2 +-
 tests/Query/EscaperTest.php                     |  1 +
 tests/Redis/Client/ClientFacadeTest.php         |  1 +
 tests/Redis/Command/AggregateTest.php           |  2 +-
 tests/Redis/Command/AliasAddTest.php            |  2 +-
 tests/Redis/Command/AliasDelTest.php            |  2 +-
 tests/Redis/Command/AliasUpdateTest.php         |  2 +-
 tests/Redis/Command/AlterTest.php               |  2 +-
 tests/Redis/Command/DictAddTest.php             |  2 +-
 tests/Redis/Command/DictDelTest.php             |  2 +-
 tests/Redis/Command/DictDumpTest.php            |  2 +-
 tests/Redis/Command/DropIndexTest.php           |  2 +-
 tests/Redis/Command/ExplainCliTest.php          |  2 +-
 tests/Redis/Command/ExplainTest.php             |  2 +-
 tests/Redis/Command/InfoTest.php                |  2 +-
 tests/Redis/Command/SearchTest.php              |  2 +-
 tests/Redis/Command/SpellCheckTest.php          |  2 +-
 tests/Redis/Command/SugAddTest.php              |  2 +-
 tests/Redis/Command/SugDelTest.php              |  2 +-
 tests/Redis/Command/SugGetTest.php              |  2 +-
 tests/Redis/Command/SugLenTest.php              |  2 +-
 tests/Redis/Command/SynDumpTest.php             |  2 +-
 tests/Redis/Command/SynUpdateTest.php           |  2 +-
 tests/Redis/Command/TagValsTest.php             |  2 +-
 tests/Redis/Response/PaginatedResponseTest.php  |  1 +
 tests/integration/DockerTest.php                |  5 +++--
 69 files changed, 103 insertions(+), 54 deletions(-)

diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index a5e9991..aac2fbe 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -48,8 +48,10 @@
         'no_alias_functions' => ['sets' => ['@all']],
         'no_php4_constructor' => true,
         'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => true],
+        'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'others', 'return', 'switch_case', 'yield', 'yield_from']],
         'no_unreachable_default_argument_value' => true,
         'no_useless_sprintf' => true,
+        'ordered_imports' => ['imports_order' => ['const', 'function', 'class']],
         'php_unit_dedicate_assert' => true,
         'php_unit_dedicate_assert_internal_type' => true,
         'php_unit_expectation' => true,
@@ -71,4 +73,4 @@
             ->in(__DIR__.'/src')
             ->in(__DIR__.'/tests')
     )
-    ;
+;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f43fdc..7cb4c2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Rework paginated responses
 - Add warning for Predis commands initialization
 - (dev) Remove deprecated `--no-suggest` option of Composer in the `Makefile`
+- (dev) Update PHP-CS-Fixer version
 
 ### Deprecated
 
diff --git a/src/Exception/OutOfRangeLevenshteinDistanceException.php b/src/Exception/OutOfRangeLevenshteinDistanceException.php
index 10b0c36..b21a6d6 100644
--- a/src/Exception/OutOfRangeLevenshteinDistanceException.php
+++ b/src/Exception/OutOfRangeLevenshteinDistanceException.php
@@ -21,8 +21,9 @@
 
 namespace MacFJA\RediSearch\Exception;
 
-use OutOfRangeException;
 use function sprintf;
+
+use OutOfRangeException;
 use Throwable;
 
 class OutOfRangeLevenshteinDistanceException extends OutOfRangeException
diff --git a/src/Exception/UnexpectedServerResponseException.php b/src/Exception/UnexpectedServerResponseException.php
index 7d0b9d8..2fd5e02 100644
--- a/src/Exception/UnexpectedServerResponseException.php
+++ b/src/Exception/UnexpectedServerResponseException.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Exception;
 
 use function is_string;
+
 use UnexpectedValueException;
 
 class UnexpectedServerResponseException extends UnexpectedValueException
diff --git a/src/Exception/UnknownUnitException.php b/src/Exception/UnknownUnitException.php
index e80403c..26f2683 100644
--- a/src/Exception/UnknownUnitException.php
+++ b/src/Exception/UnknownUnitException.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Exception;
 
 use function sprintf;
+
 use Throwable;
 use UnexpectedValueException;
 
diff --git a/src/Index.php b/src/Index.php
index 02d269e..63ef764 100644
--- a/src/Index.php
+++ b/src/Index.php
@@ -24,6 +24,7 @@
 use function count;
 use function is_array;
 use function is_string;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\AliasAdd;
diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php
index 78674b7..74bb31f 100644
--- a/src/IndexBuilder.php
+++ b/src/IndexBuilder.php
@@ -21,14 +21,16 @@
 
 namespace MacFJA\RediSearch;
 
-use BadMethodCallException;
 use function count;
 use function in_array;
-use InvalidArgumentException;
 use function is_array;
 use function is_float;
 use function is_int;
 use function is_string;
+use function strlen;
+
+use BadMethodCallException;
+use InvalidArgumentException;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait;
@@ -36,7 +38,6 @@
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
 use RuntimeException;
-use function strlen;
 
 /**
  * @method IndexBuilder setIndex(string $name)
diff --git a/src/Query/Builder/AndGroup.php b/src/Query/Builder/AndGroup.php
index 9072f67..8a96425 100644
--- a/src/Query/Builder/AndGroup.php
+++ b/src/Query/Builder/AndGroup.php
@@ -28,7 +28,7 @@ class AndGroup extends AbstractGroup
     public function render(?callable $escaper = null): string
     {
         if (1 === count($this->elements)) {
-            return (reset($this->elements))->render($escaper);
+            return reset($this->elements)->render($escaper);
         }
 
         return implode(' ', array_map(static function (QueryElement $element) use ($escaper): string {
diff --git a/src/Query/Builder/FieldFacet.php b/src/Query/Builder/FieldFacet.php
index 2a22685..2541a7c 100644
--- a/src/Query/Builder/FieldFacet.php
+++ b/src/Query/Builder/FieldFacet.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function count;
+
 use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Query\Escaper;
 
@@ -49,7 +50,7 @@ public function render(?callable $escaper = null): string
     {
         return sprintf(
             '@%s:%s',
-            (OrGroup::fromStrings(...$this->fields)->render()),
+            OrGroup::fromStrings(...$this->fields)->render(),
             EncapsulationGroup::simple($this->element)->render()
         );
     }
diff --git a/src/Query/Builder/GeoFacet.php b/src/Query/Builder/GeoFacet.php
index 6d404fa..8d00db1 100644
--- a/src/Query/Builder/GeoFacet.php
+++ b/src/Query/Builder/GeoFacet.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function count;
+
 use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Exception\UnknownUnitException;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
diff --git a/src/Query/Builder/NumericFacet.php b/src/Query/Builder/NumericFacet.php
index 87c9255..3028424 100644
--- a/src/Query/Builder/NumericFacet.php
+++ b/src/Query/Builder/NumericFacet.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function is_array;
+
 use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\NumericRangeTrait;
 
diff --git a/src/Query/Builder/QueryElementAttribute.php b/src/Query/Builder/QueryElementAttribute.php
index 4f292b6..40667d6 100644
--- a/src/Query/Builder/QueryElementAttribute.php
+++ b/src/Query/Builder/QueryElementAttribute.php
@@ -26,9 +26,10 @@
 use function is_bool;
 use function is_float;
 use function is_int;
-use NumberFormatter;
 use function sprintf;
 
+use NumberFormatter;
+
 class QueryElementAttribute implements QueryElement, QueryElementDecorator
 {
     /** @var QueryElement */
diff --git a/src/Query/Builder/TagFacet.php b/src/Query/Builder/TagFacet.php
index 96a0920..bf28da1 100644
--- a/src/Query/Builder/TagFacet.php
+++ b/src/Query/Builder/TagFacet.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function count;
+
 use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Exception\NotEnoughTermsException;
 
diff --git a/src/Query/Builder/TextFacet.php b/src/Query/Builder/TextFacet.php
index 61750a5..3d549fd 100644
--- a/src/Query/Builder/TextFacet.php
+++ b/src/Query/Builder/TextFacet.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Query\Builder;
 
 use function count;
+
 use MacFJA\RediSearch\Exception\NotEnoughFieldsException;
 use MacFJA\RediSearch\Exception\NotEnoughTermsException;
 
diff --git a/src/Redis/Client/AbstractClient.php b/src/Redis/Client/AbstractClient.php
index 3856f3e..3f12b39 100644
--- a/src/Redis/Client/AbstractClient.php
+++ b/src/Redis/Client/AbstractClient.php
@@ -22,9 +22,10 @@
 namespace MacFJA\RediSearch\Redis\Client;
 
 use function count;
+use function strlen;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
-use function strlen;
 
 abstract class AbstractClient implements Client
 {
diff --git a/src/Redis/Client/AmpRedisClient.php b/src/Redis/Client/AmpRedisClient.php
index f94d878..540641c 100644
--- a/src/Redis/Client/AmpRedisClient.php
+++ b/src/Redis/Client/AmpRedisClient.php
@@ -21,10 +21,11 @@
 
 namespace MacFJA\RediSearch\Redis\Client;
 
-use Amp\Promise;
 use function Amp\Promise\wait;
-use Amp\Redis\Redis;
 use function function_exists;
+
+use Amp\Promise;
+use Amp\Redis\Redis;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
diff --git a/src/Redis/Client/CredisClient.php b/src/Redis/Client/CredisClient.php
index 05c2cde..2569e09 100644
--- a/src/Redis/Client/CredisClient.php
+++ b/src/Redis/Client/CredisClient.php
@@ -21,8 +21,9 @@
 
 namespace MacFJA\RediSearch\Redis\Client;
 
-use Credis_Client;
 use function is_array;
+
+use Credis_Client;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
diff --git a/src/Redis/Client/PhpiredisClient.php b/src/Redis/Client/PhpiredisClient.php
index 8fe34a9..d371fb7 100644
--- a/src/Redis/Client/PhpiredisClient.php
+++ b/src/Redis/Client/PhpiredisClient.php
@@ -21,9 +21,10 @@
 
 namespace MacFJA\RediSearch\Redis\Client;
 
-use Exception;
 use function function_exists;
 use function is_resource;
+
+use Exception;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use RuntimeException;
diff --git a/src/Redis/Client/PhpredisClient.php b/src/Redis/Client/PhpredisClient.php
index 15194d3..d20c5dd 100644
--- a/src/Redis/Client/PhpredisClient.php
+++ b/src/Redis/Client/PhpredisClient.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Client;
 
 use function count;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use Redis;
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
index 51ad1dd..adc1fe0 100644
--- a/src/Redis/Client/PredisClient.php
+++ b/src/Redis/Client/PredisClient.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Client;
 
 use function assert;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use Predis\ClientContextInterface;
diff --git a/src/Redis/Client/Rediska/RediskaRediSearchCommand.php b/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
index 9abadf6..08996e3 100644
--- a/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
+++ b/src/Redis/Client/Rediska/RediskaRediSearchCommand.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Client\Rediska;
 
 use function count;
+
 use MacFJA\RediSearch\Redis\Client\AbstractClient;
 use Rediska_Command_Abstract;
 use Rediska_Connection_Exec;
diff --git a/src/Redis/Client/RediskaClient.php b/src/Redis/Client/RediskaClient.php
index aa7ae31..6459ff1 100644
--- a/src/Redis/Client/RediskaClient.php
+++ b/src/Redis/Client/RediskaClient.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Client;
 
 use function in_array;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command;
 use MacFJA\RediSearch\Redis\Initializer;
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index c9855f3..ca53c69 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -22,8 +22,9 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function count;
-use InvalidArgumentException;
 use function is_array;
+
+use InvalidArgumentException;
 use MacFJA\RediSearch\Redis\Command;
 use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
 
diff --git a/src/Redis/Command/AddFieldOptionTrait.php b/src/Redis/Command/AddFieldOptionTrait.php
index 451c1e6..ced4117 100644
--- a/src/Redis/Command/AddFieldOptionTrait.php
+++ b/src/Redis/Command/AddFieldOptionTrait.php
@@ -21,8 +21,9 @@
 
 namespace MacFJA\RediSearch\Redis\Command;
 
-use BadMethodCallException;
 use function get_class;
+
+use BadMethodCallException;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 0cf110d..241f17f 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -24,6 +24,7 @@
 use function assert;
 use function count;
 use function is_array;
+
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
diff --git a/src/Redis/Command/AggregateCommand/GroupByOption.php b/src/Redis/Command/AggregateCommand/GroupByOption.php
index 4441c59..c68ceec 100644
--- a/src/Redis/Command/AggregateCommand/GroupByOption.php
+++ b/src/Redis/Command/AggregateCommand/GroupByOption.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
 
 use function count;
+
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 use MacFJA\RediSearch\Redis\Command\PrefixFieldName;
 
diff --git a/src/Redis/Command/AggregateCommand/ReduceOption.php b/src/Redis/Command/AggregateCommand/ReduceOption.php
index 7f57973..bb2eba3 100644
--- a/src/Redis/Command/AggregateCommand/ReduceOption.php
+++ b/src/Redis/Command/AggregateCommand/ReduceOption.php
@@ -24,6 +24,7 @@
 use function count;
 use function is_bool;
 use function is_string;
+
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 use MacFJA\RediSearch\Redis\Command\PrefixFieldName;
 
diff --git a/src/Redis/Command/AggregateCommand/SortByOption.php b/src/Redis/Command/AggregateCommand/SortByOption.php
index ea9b8b0..7b54bc4 100644
--- a/src/Redis/Command/AggregateCommand/SortByOption.php
+++ b/src/Redis/Command/AggregateCommand/SortByOption.php
@@ -23,6 +23,7 @@
 
 use function count;
 use function is_int;
+
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 use MacFJA\RediSearch\Redis\Command\PrefixFieldName;
 
diff --git a/src/Redis/Command/CursorRead.php b/src/Redis/Command/CursorRead.php
index a380166..3345290 100644
--- a/src/Redis/Command/CursorRead.php
+++ b/src/Redis/Command/CursorRead.php
@@ -25,6 +25,7 @@
 use function count;
 use function is_array;
 use function is_int;
+
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
diff --git a/src/Redis/Command/Info.php b/src/Redis/Command/Info.php
index 50b4b42..c90f741 100644
--- a/src/Redis/Command/Info.php
+++ b/src/Redis/Command/Info.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function is_array;
+
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Response\InfoResponse;
diff --git a/src/Redis/Command/Option/AbstractCommandOption.php b/src/Redis/Command/Option/AbstractCommandOption.php
index cd37c61..e2b9966 100644
--- a/src/Redis/Command/Option/AbstractCommandOption.php
+++ b/src/Redis/Command/Option/AbstractCommandOption.php
@@ -21,8 +21,9 @@
 
 namespace MacFJA\RediSearch\Redis\Command\Option;
 
-use Composer\Semver\Semver;
 use function is_scalar;
+
+use Composer\Semver\Semver;
 use UnexpectedValueException;
 
 abstract class AbstractCommandOption implements CommandOption
diff --git a/src/Redis/Command/Option/GroupedOption.php b/src/Redis/Command/Option/GroupedOption.php
index 9b91a1e..088ed7a 100644
--- a/src/Redis/Command/Option/GroupedOption.php
+++ b/src/Redis/Command/Option/GroupedOption.php
@@ -22,10 +22,11 @@
 namespace MacFJA\RediSearch\Redis\Command\Option;
 
 use function array_key_exists;
-use BadMethodCallException;
 use function in_array;
 use function is_bool;
 use function is_scalar;
+
+use BadMethodCallException;
 use OutOfRangeException;
 
 class GroupedOption extends AbstractCommandOption
diff --git a/src/Redis/Command/Option/NotEmptyOption.php b/src/Redis/Command/Option/NotEmptyOption.php
index c7969e9..76029ad 100644
--- a/src/Redis/Command/Option/NotEmptyOption.php
+++ b/src/Redis/Command/Option/NotEmptyOption.php
@@ -29,7 +29,10 @@
  */
 class NotEmptyOption implements CommandOption, DecoratedCommandOption
 {
-    use DecoratedOptionTrait { isValid as decoratedIsValid; render as decoratedRender; }
+    use DecoratedOptionTrait {
+        isValid as decoratedIsValid;
+        render as decoratedRender;
+    }
 
     /**
      * @phpstan-param T $decorated
diff --git a/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php b/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
index 5618171..58d2ba3 100644
--- a/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
+++ b/src/Redis/Command/Option/WithPublicGroupedSetterTrait.php
@@ -21,10 +21,11 @@
 
 namespace MacFJA\RediSearch\Redis\Command\Option;
 
-use BadMethodCallException;
 use function count;
 use function get_class;
 use function in_array;
+
+use BadMethodCallException;
 use InvalidArgumentException;
 
 trait WithPublicGroupedSetterTrait
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 15008c5..9cf4fab 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -24,6 +24,7 @@
 use function assert;
 use function count;
 use function is_array;
+
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
diff --git a/src/Redis/Command/SearchCommand/LimitOption.php b/src/Redis/Command/SearchCommand/LimitOption.php
index d819fd0..fb3cb73 100644
--- a/src/Redis/Command/SearchCommand/LimitOption.php
+++ b/src/Redis/Command/SearchCommand/LimitOption.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command\SearchCommand;
 
 use function is_int;
+
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 
 class LimitOption extends AbstractCommandOption
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
index ed30225..45e2e97 100644
--- a/src/Redis/Command/SpellCheck.php
+++ b/src/Redis/Command/SpellCheck.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command;
 
 use function is_array;
+
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
diff --git a/src/Redis/Command/SpellCheckCommand/TermsOption.php b/src/Redis/Command/SpellCheckCommand/TermsOption.php
index 3b69f8d..3dae047 100644
--- a/src/Redis/Command/SpellCheckCommand/TermsOption.php
+++ b/src/Redis/Command/SpellCheckCommand/TermsOption.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command\SpellCheckCommand;
 
 use function is_string;
+
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 
 class TermsOption extends AbstractCommandOption
diff --git a/src/Redis/Command/SugGet.php b/src/Redis/Command/SugGet.php
index 5d260f6..f8d20ac 100644
--- a/src/Redis/Command/SugGet.php
+++ b/src/Redis/Command/SugGet.php
@@ -23,6 +23,7 @@
 
 use function count;
 use function is_array;
+
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php
index 7bbdc26..4e024e1 100644
--- a/src/Redis/Response/CursorResponse.php
+++ b/src/Redis/Response/CursorResponse.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Response;
 
 use function count;
+
 use Countable;
 use Iterator;
 use MacFJA\RediSearch\Exception\MissingClientException;
@@ -126,7 +127,7 @@ public function key()
             return 0;
         }
 
-        return (int) ceil(($this->offset) / $this->size) + 1;
+        return (int) ceil($this->offset / $this->size) + 1;
     }
 
     public function valid()
diff --git a/src/Redis/Response/InfoResponse.php b/src/Redis/Response/InfoResponse.php
index 410d835..6ca4636 100644
--- a/src/Redis/Response/InfoResponse.php
+++ b/src/Redis/Response/InfoResponse.php
@@ -22,12 +22,13 @@
 namespace MacFJA\RediSearch\Redis\Response;
 
 use function assert;
-use BadMethodCallException;
 use function in_array;
-use InvalidArgumentException;
 use function is_array;
 use function is_object;
 use function is_string;
+
+use BadMethodCallException;
+use InvalidArgumentException;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption;
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index 945c8dc..126aa30 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -22,8 +22,9 @@
 namespace MacFJA\RediSearch\Redis\Response;
 
 use function count;
-use Countable;
 use function is_int;
+
+use Countable;
 use Iterator;
 use MacFJA\RediSearch\Exception\MissingClientException;
 use MacFJA\RediSearch\Redis\Client;
@@ -112,13 +113,13 @@ public function key()
     public function valid()
     {
         return (
-                null === $this->requestedOffset
-                && null === $this->requestedSize
-            ) || (
-                $this->requestedOffset >= 0
-                && $this->requestedSize > 0
-                && $this->requestedOffset < $this->totalCount
-            );
+            null === $this->requestedOffset
+            && null === $this->requestedSize
+        ) || (
+            $this->requestedOffset >= 0
+            && $this->requestedSize > 0
+            && $this->requestedOffset < $this->totalCount
+        );
     }
 
     public function rewind(): void
diff --git a/src/Suggestions.php b/src/Suggestions.php
index 91c0732..af2fa2d 100644
--- a/src/Suggestions.php
+++ b/src/Suggestions.php
@@ -23,6 +23,7 @@
 
 use function is_int;
 use function is_string;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\SugAdd;
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index 039088f..6c23831 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -219,7 +219,7 @@ public function testDocFuzzyExample1(): void
         $builder
             ->addElement(new Builder\Fuzzy('hello'))
             ->addString('world')
-            ;
+        ;
 
         static::assertSame($expected, $builder->render());
     }
diff --git a/tests/Query/EscaperTest.php b/tests/Query/EscaperTest.php
index bf3ac8e..bcbcb80 100644
--- a/tests/Query/EscaperTest.php
+++ b/tests/Query/EscaperTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Query;
 
 use function count;
+
 use MacFJA\RediSearch\Query\Escaper;
 
 /**
diff --git a/tests/Redis/Client/ClientFacadeTest.php b/tests/Redis/Client/ClientFacadeTest.php
index 054096d..ae5e1cd 100644
--- a/tests/Redis/Client/ClientFacadeTest.php
+++ b/tests/Redis/Client/ClientFacadeTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Redis\Client;
 
 use function get_class;
+
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Client\ClientFacade;
 use MacFJA\RediSearch\Redis\Command;
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index afd4192..6688d0e 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -137,7 +137,7 @@ public function testFullOption(): void
             ->setLimit(12, 35)
             ->addApply('@timestamp - (@timestamp % 86400)', 'day')
             ->setWithCursor(20, 30)
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/AliasAddTest.php b/tests/Redis/Command/AliasAddTest.php
index 12b07d2..fee17ac 100644
--- a/tests/Redis/Command/AliasAddTest.php
+++ b/tests/Redis/Command/AliasAddTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->setAlias('current')
-            ;
+        ;
 
         static::assertSame([
             'current', 'idx',
diff --git a/tests/Redis/Command/AliasDelTest.php b/tests/Redis/Command/AliasDelTest.php
index 05dba5d..9ac2379 100644
--- a/tests/Redis/Command/AliasDelTest.php
+++ b/tests/Redis/Command/AliasDelTest.php
@@ -47,7 +47,7 @@ public function testFullOption(): void
         $command = new AliasDel();
         $command
             ->setAlias('current')
-            ;
+        ;
 
         static::assertSame([
             'current',
diff --git a/tests/Redis/Command/AliasUpdateTest.php b/tests/Redis/Command/AliasUpdateTest.php
index 12c17f1..d9a6596 100644
--- a/tests/Redis/Command/AliasUpdateTest.php
+++ b/tests/Redis/Command/AliasUpdateTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->setAlias('current')
-            ;
+        ;
 
         static::assertSame([
             'current', 'idx',
diff --git a/tests/Redis/Command/AlterTest.php b/tests/Redis/Command/AlterTest.php
index 4ab859b..d2a4fd0 100644
--- a/tests/Redis/Command/AlterTest.php
+++ b/tests/Redis/Command/AlterTest.php
@@ -54,7 +54,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->addNumericField('id2', true, false)
-            ;
+        ;
 
         static::assertSame([
             'idx', 'SCHEMA', 'ADD', 'id2', 'NUMERIC', 'SORTABLE',
diff --git a/tests/Redis/Command/DictAddTest.php b/tests/Redis/Command/DictAddTest.php
index ad8d648..4920b49 100644
--- a/tests/Redis/Command/DictAddTest.php
+++ b/tests/Redis/Command/DictAddTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setDictionary('cities')
             ->addTerms('paris', 'new york', 'london')
-            ;
+        ;
 
         static::assertSame([
             'cities',
diff --git a/tests/Redis/Command/DictDelTest.php b/tests/Redis/Command/DictDelTest.php
index f4a1c1e..a625cbc 100644
--- a/tests/Redis/Command/DictDelTest.php
+++ b/tests/Redis/Command/DictDelTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setDictionary('cities')
             ->addTerms('paris', 'new york', 'london')
-            ;
+        ;
 
         static::assertSame([
             'cities',
diff --git a/tests/Redis/Command/DictDumpTest.php b/tests/Redis/Command/DictDumpTest.php
index c0e533f..33dc481 100644
--- a/tests/Redis/Command/DictDumpTest.php
+++ b/tests/Redis/Command/DictDumpTest.php
@@ -47,7 +47,7 @@ public function testFullOption(): void
         $command = new DictDump();
         $command
             ->setDictionary('cities')
-            ;
+        ;
 
         static::assertSame([
             'cities',
diff --git a/tests/Redis/Command/DropIndexTest.php b/tests/Redis/Command/DropIndexTest.php
index 384b969..88391cd 100644
--- a/tests/Redis/Command/DropIndexTest.php
+++ b/tests/Redis/Command/DropIndexTest.php
@@ -49,7 +49,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->setDeleteDocument(true)
-            ;
+        ;
 
         static::assertSame([
             'idx', 'DD',
diff --git a/tests/Redis/Command/ExplainCliTest.php b/tests/Redis/Command/ExplainCliTest.php
index 1569fab..3f38723 100644
--- a/tests/Redis/Command/ExplainCliTest.php
+++ b/tests/Redis/Command/ExplainCliTest.php
@@ -49,7 +49,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/ExplainTest.php b/tests/Redis/Command/ExplainTest.php
index bc15225..9497ef8 100644
--- a/tests/Redis/Command/ExplainTest.php
+++ b/tests/Redis/Command/ExplainTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/InfoTest.php b/tests/Redis/Command/InfoTest.php
index d1b9b36..7e63472 100644
--- a/tests/Redis/Command/InfoTest.php
+++ b/tests/Redis/Command/InfoTest.php
@@ -50,7 +50,7 @@ public function testFullOption(): void
         $command = new Info();
         $command
             ->setIndex('idx')
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index 944dba2..6fc6e5c 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -102,7 +102,7 @@ public function testFullOption(): void
             ->setPayload('foobar')
             ->setSortBy('@num1', 'ASC')
             ->setLimit(12, 10)
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/SpellCheckTest.php b/tests/Redis/Command/SpellCheckTest.php
index 5ae27b3..02a387b 100644
--- a/tests/Redis/Command/SpellCheckTest.php
+++ b/tests/Redis/Command/SpellCheckTest.php
@@ -58,7 +58,7 @@ public function testFullOption(): void
             ->setDistance(2)
             ->addTerms('badword', true)
             ->addTerms('cities', false)
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/SugAddTest.php b/tests/Redis/Command/SugAddTest.php
index cf6eae9..152ad4d 100644
--- a/tests/Redis/Command/SugAddTest.php
+++ b/tests/Redis/Command/SugAddTest.php
@@ -54,7 +54,7 @@ public function testFullOption(): void
             ->setScore(1)
             ->setIncrement()
             ->setPayload('greeting')
-            ;
+        ;
 
         static::assertSame([
             'ac', 'hello world', 1.0, 'INCR', 'PAYLOAD', 'greeting',
diff --git a/tests/Redis/Command/SugDelTest.php b/tests/Redis/Command/SugDelTest.php
index 17e2cd8..e449c0b 100644
--- a/tests/Redis/Command/SugDelTest.php
+++ b/tests/Redis/Command/SugDelTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setDictionary('ac')
             ->setSuggestion('hello world')
-            ;
+        ;
 
         static::assertSame([
             'ac', 'hello world',
diff --git a/tests/Redis/Command/SugGetTest.php b/tests/Redis/Command/SugGetTest.php
index b6a8acd..2f6fde7 100644
--- a/tests/Redis/Command/SugGetTest.php
+++ b/tests/Redis/Command/SugGetTest.php
@@ -58,7 +58,7 @@ public function testFullOption(): void
             ->setWithScores()
             ->setWithPayloads()
             ->setMax(10)
-            ;
+        ;
 
         static::assertSame([
             'ac', 'hell', 'FUZZY', 'WITHSCORES', 'WITHPAYLOADS', 'MAX', 10,
diff --git a/tests/Redis/Command/SugLenTest.php b/tests/Redis/Command/SugLenTest.php
index 6ff5c72..c71ac59 100644
--- a/tests/Redis/Command/SugLenTest.php
+++ b/tests/Redis/Command/SugLenTest.php
@@ -47,7 +47,7 @@ public function testFullOption(): void
         $command = new SugLen();
         $command
             ->setDictionary('ac')
-            ;
+        ;
 
         static::assertSame([
             'ac',
diff --git a/tests/Redis/Command/SynDumpTest.php b/tests/Redis/Command/SynDumpTest.php
index f3e24cc..994f9e3 100644
--- a/tests/Redis/Command/SynDumpTest.php
+++ b/tests/Redis/Command/SynDumpTest.php
@@ -47,7 +47,7 @@ public function testFullOption(): void
         $command = new SynDump();
         $command
             ->setIndex('idx')
-            ;
+        ;
 
         static::assertSame([
             'idx',
diff --git a/tests/Redis/Command/SynUpdateTest.php b/tests/Redis/Command/SynUpdateTest.php
index 7490514..a7a851d 100644
--- a/tests/Redis/Command/SynUpdateTest.php
+++ b/tests/Redis/Command/SynUpdateTest.php
@@ -51,7 +51,7 @@ public function testFullOption(): void
             ->setGroupId('blue')
             ->setSkipInitialScan()
             ->setTerms('blue', 'cyan', 'navy', 'teal')
-            ;
+        ;
 
         static::assertSame([
             'idx', 'blue', 'SKIPINITIALSCAN', 'blue',  'cyan', 'navy', 'teal',
diff --git a/tests/Redis/Command/TagValsTest.php b/tests/Redis/Command/TagValsTest.php
index 9df66f9..3a0db8d 100644
--- a/tests/Redis/Command/TagValsTest.php
+++ b/tests/Redis/Command/TagValsTest.php
@@ -48,7 +48,7 @@ public function testFullOption(): void
         $command
             ->setIndex('idx')
             ->setField('myTag')
-            ;
+        ;
 
         static::assertSame([
             'idx', 'myTag',
diff --git a/tests/Redis/Response/PaginatedResponseTest.php b/tests/Redis/Response/PaginatedResponseTest.php
index d85ac3e..445a760 100644
--- a/tests/Redis/Response/PaginatedResponseTest.php
+++ b/tests/Redis/Response/PaginatedResponseTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Redis\Response;
 
 use function array_slice;
+
 use MacFJA\RediSearch\Exception\MissingClientException;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\PaginatedCommand;
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 5426537..f9c3929 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -21,12 +21,13 @@
 
 namespace MacFJA\RediSearch\tests\integration;
 
+use function get_class;
+use function is_string;
+
 use Amp\Redis\Config;
 use Amp\Redis\RemoteExecutor;
 use Closure;
 use Credis_Client;
-use function get_class;
-use function is_string;
 use MacFJA\RediSearch\Index;
 use MacFJA\RediSearch\IndexBuilder;
 use MacFJA\RediSearch\Query\Builder;

From a90422bfb4f3d426dc7172ef9dbb292692a1f9f3 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 24 Jul 2022 19:09:21 +0200
Subject: [PATCH 46/60] Improvement for \MacFJA\RediSearch\Index class

---
 CHANGELOG.md                              |   6 +
 src/Index.php                             |  39 ++--
 src/Redis/Response/InfoResponse.php       |   4 +-
 tests/IndexTest.php                       | 269 ++++++++++++++++++++++
 tests/Redis/Response/InfoResponseTest.php |   8 +
 tests/integration/DockerTest.php          |  63 ++++-
 6 files changed, 371 insertions(+), 18 deletions(-)
 create mode 100644 tests/IndexTest.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cb4c2e..118234c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,15 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Throw custom exception is client is missing
 - Add compatibility for Predis version 2.x
 - (dev) Add `make clean` to remove all generated files
+- (dev) Add code coverage to `\MacFJA\RediSearch\Index`
+- (dev) Add integration test for document insertion
 
 ### Fixed
 
 - Missing default values on highlight option ([Issue#38])
+- Don't throw an exception if the result of the index definition is incorrect ([Issue#46])
 
 ### Changed
 
 - Rework paginated responses
 - Add warning for Predis commands initialization
+- Lazy load RediSearch version and index information in `\MacFJA\RediSearch\Index`
+- Allow to provide RediSearch version manually to `\MacFJA\RediSearch\Index`
 - (dev) Remove deprecated `--no-suggest` option of Composer in the `Makefile`
 - (dev) Update PHP-CS-Fixer version
 
@@ -227,6 +232,7 @@ First version
 [Issue#26]: https://github.com/MacFJA/php-redisearch/issues/26
 [Issue#38]: https://github.com/MacFJA/php-redisearch/issues/38
 [Issue#39]: https://github.com/MacFJA/php-redisearch/issues/39
+[Issue#46]: https://github.com/MacFJA/php-redisearch/issues/46
 [PR#1]: https://github.com/MacFJA/php-redisearch/pull/1
 [PR#3]: https://github.com/MacFJA/php-redisearch/pull/3
 [PR#8]: https://github.com/MacFJA/php-redisearch/pull/8
diff --git a/src/Index.php b/src/Index.php
index 63ef764..c24fd31 100644
--- a/src/Index.php
+++ b/src/Index.php
@@ -38,29 +38,25 @@
 use MacFJA\RediSearch\Redis\Initializer;
 use MacFJA\RediSearch\Redis\Response\InfoResponse;
 
-/**
- * @codeCoverageIgnore
- */
 class Index
 {
     /** @var Client */
     private $client;
 
-    /** @var InfoResponse */
+    /** @var null|InfoResponse */
     private $info;
 
     /** @var string */
     private $index;
 
-    /** @var string */
+    /** @var null|string */
     private $version;
 
-    public function __construct(string $index, Client $client)
+    public function __construct(string $index, Client $client, ?string $version = null)
     {
         $this->client = $client;
         $this->index = $index;
-        $this->version = Initializer::getRediSearchVersion($client) ?? AbstractCommand::MIN_IMPLEMENTED_VERSION;
-        $this->getInfo();
+        $this->version = $version;
     }
 
     /**
@@ -68,7 +64,7 @@ public function __construct(string $index, Client $client)
      */
     public function addDocumentFromArray(array $properties, ?string $hash = null): string
     {
-        $prefixes = $this->info->getIndexDefinition('prefixes');
+        $prefixes = $this->getInfo()->getIndexDefinition('prefixes');
         $prefix = '';
         if (is_array($prefixes) && count($prefixes) > 0) {
             $prefix = (string) reset($prefixes);
@@ -94,7 +90,7 @@ public function deleteDocument(string $hash): bool
 
     public function addField(CreateCommandFieldOption $field): bool
     {
-        $command = new Alter($this->version);
+        $command = new Alter($this->getVersion());
         $command
             ->setIndex($this->index)
             ->addField($field)
@@ -105,7 +101,7 @@ public function addField(CreateCommandFieldOption $field): bool
 
     public function delete(bool $withDocuments = false): bool
     {
-        $command = new DropIndex($this->version);
+        $command = new DropIndex($this->getVersion());
         $command->setIndex($this->index)
             ->setDeleteDocument($withDocuments)
         ;
@@ -116,7 +112,7 @@ public function delete(bool $withDocuments = false): bool
     public function addAlias(string $alias): bool
     {
         return 'OK' === (string) $this->client->execute(
-            (new AliasAdd($this->version))
+            (new AliasAdd($this->getVersion()))
                 ->setIndex($this->index)
                 ->setAlias($alias)
         );
@@ -124,12 +120,12 @@ public function addAlias(string $alias): bool
 
     public function updateAlias(string $alias): bool
     {
-        return 'OK' === (string) $this->client->execute((new AliasUpdate($this->version))->setIndex($this->index)->setAlias($alias));
+        return 'OK' === (string) $this->client->execute((new AliasUpdate($this->getVersion()))->setIndex($this->index)->setAlias($alias));
     }
 
     public function deleteAlias(string $alias): bool
     {
-        return 'OK' === (string) $this->client->execute((new AliasDel($this->version))->setAlias($alias));
+        return 'OK' === (string) $this->client->execute((new AliasDel($this->getVersion()))->setAlias($alias));
     }
 
     /**
@@ -137,13 +133,24 @@ public function deleteAlias(string $alias): bool
      */
     public function getTagValues(string $fieldName): array
     {
-        return $this->client->execute((new TagVals($this->version))->setIndex($this->index)->setField($fieldName));
+        return $this->client->execute((new TagVals($this->getVersion()))->setIndex($this->index)->setField($fieldName));
     }
 
     public function getInfo(): InfoResponse
     {
-        $this->info = $this->client->execute((new Info($this->version))->setIndex($this->index));
+        if (!($this->info instanceof InfoResponse)) {
+            $this->info = $this->client->execute((new Info($this->getVersion()))->setIndex($this->index));
+        }
 
         return $this->info;
     }
+
+    private function getVersion(): string
+    {
+        if (!is_string($this->version)) {
+            $this->version = Initializer::getRediSearchVersion($this->client) ?? AbstractCommand::MIN_IMPLEMENTED_VERSION;
+        }
+
+        return $this->version;
+    }
 }
diff --git a/src/Redis/Response/InfoResponse.php b/src/Redis/Response/InfoResponse.php
index 6ca4636..897d7ef 100644
--- a/src/Redis/Response/InfoResponse.php
+++ b/src/Redis/Response/InfoResponse.php
@@ -171,7 +171,9 @@ public function getFieldsAsOption(): array
     public function getIndexDefinition(?string $key = null)
     {
         $rawDefinitions = self::getValue($this->rawLines, 'index_definition');
-        assert(is_array($rawDefinitions));
+        if (!is_array($rawDefinitions)) {
+            return is_string($key) ? null : [];
+        }
         $data = self::getPairs($rawDefinitions);
         if (!is_string($key)) {
             return $data;
diff --git a/tests/IndexTest.php b/tests/IndexTest.php
new file mode 100644
index 0000000..51a9e1e
--- /dev/null
+++ b/tests/IndexTest.php
@@ -0,0 +1,269 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests;
+
+use MacFJA\RediSearch\Index;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Command\AbstractCommand;
+use MacFJA\RediSearch\Redis\Command\AliasAdd;
+use MacFJA\RediSearch\Redis\Command\AliasDel;
+use MacFJA\RediSearch\Redis\Command\AliasUpdate;
+use MacFJA\RediSearch\Redis\Command\Alter;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use MacFJA\RediSearch\Redis\Command\DropIndex;
+use MacFJA\RediSearch\Redis\Command\TagVals;
+use MacFJA\RediSearch\Redis\Response\InfoResponse;
+
+/**
+ * @covers \MacFJA\RediSearch\Index
+ *
+ * @uses \MacFJA\RediSearch\Redis\Response\ArrayResponseTrait
+ * @uses \MacFJA\RediSearch\Redis\Response\InfoResponse
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Alter
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\BaseCreateFieldOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\DropIndex
+ * @uses \MacFJA\RediSearch\Redis\Command\AliasAdd
+ * @uses \MacFJA\RediSearch\Redis\Command\AliasUpdate
+ * @uses \MacFJA\RediSearch\Redis\Command\AliasDel
+ * @uses \MacFJA\RediSearch\Redis\Command\TagVals
+ *
+ * @internal
+ */
+class IndexTest extends \PHPUnit\Framework\TestCase
+{
+    public function testAddDocumentFromArray(): void
+    {
+        $expectedKey = 'manualKey';
+        $client = $this->createMock(Client::class);
+        $client->method('executeRaw')->withConsecutive(
+            ['hset', static::stringStartsWith('doc:'), 'f1', 'foo', 'f2', 'bar'],
+            ['hset', $expectedKey, 'f1', 'foo', 'f2', 'bar']
+        );
+
+        $index = $this->getMockBuilder(Index::class)
+            ->setConstructorArgs(['idx', $client, AbstractCommand::MIN_IMPLEMENTED_VERSION])
+            ->onlyMethods(['getInfo'])
+            ->getMock()
+        ;
+        $normalInfoResponse = new InfoResponse([
+            0 => 'index_name',
+            1 => 'idxt',
+            2 => 'index_options',
+            3 => [],
+            4 => 'index_definition',
+            5 => [
+                0 => 'key_type',
+                1 => 'HASH',
+                2 => 'prefixes',
+                3 => [0 => 'doc:'],
+                4 => 'language_field',
+                5 => '__language',
+                6 => 'default_score',
+                7 => '1',
+                8 => 'score_field',
+                9 => '__score',
+                10 => 'payload_field',
+                11 => '__payload',
+            ],
+            6 => 'fields',
+            7 => [
+                0 => [
+                    0 => 'f1',
+                    1 => 'type',
+                    2 => 'TEXT',
+                    3 => 'WEIGHT',
+                    4 => '1',
+                ],
+                1 => [
+                    0 => 'f2',
+                    1 => 'type',
+                    2 => 'TEXT',
+                ],
+            ],
+            8 => 'num_docs',
+            9 => '4',
+            10 => 'max_doc_id',
+            11 => '12',
+            12 => 'num_terms',
+            13 => '4',
+            14 => 'num_records',
+            15 => '30',
+            16 => 'inverted_sz_mb',
+            17 => '0.000171661376953125',
+            18 => 'total_inverted_index_blocks',
+            19 => '1801',
+            20 => 'offset_vectors_sz_mb',
+            21 => '2.86102294921875e-05',
+            22 => 'doc_table_size_mb',
+            23 => '0.000995635986328125',
+            24 => 'sortable_values_size_mb',
+            25 => '0',
+            26 => 'key_table_size_mb',
+            27 => '0.00012493133544921875',
+            28 => 'records_per_doc_avg',
+            29 => '7.5',
+            30 => 'bytes_per_record_avg',
+            31 => '6',
+            32 => 'offsets_per_term_avg',
+            33 => '1',
+            34 => 'offset_bits_per_record_avg',
+            35 => '8',
+            36 => 'hash_indexing_failures',
+            37 => '0',
+            38 => 'indexing',
+            39 => '0',
+            40 => 'percent_indexed',
+            41 => '1',
+            42 => 'gc_stats',
+            43 => [
+                0 => 'bytes_collected',
+                1 => '0',
+                2 => 'total_ms_run',
+                3 => '0',
+                4 => 'total_cycles',
+                5 => '0',
+                6 => 'average_cycle_time_ms',
+                7 => '-nan',
+                8 => 'last_run_time_ms',
+                9 => '0',
+                10 => 'gc_numeric_trees_missed',
+                11 => '0',
+                12 => 'gc_blocks_denied',
+                13 => '0',
+            ],
+            44 => 'cursor_stats',
+            45 => [
+                0 => 'global_idle',
+                1 => 63,
+                2 => 'global_total',
+                3 => 63,
+                4 => 'index_capacity',
+                5 => 128,
+                6 => 'index_total',
+                7 => 63,
+            ],
+        ]);
+        $emptyInfoResponse = new InfoResponse([]);
+        $index->method('getInfo')->willReturnOnConsecutiveCalls($normalInfoResponse, $normalInfoResponse, $emptyInfoResponse, $emptyInfoResponse);
+
+        $generatedHash = $index->addDocumentFromArray(['f1' => 'foo', 'f2' => 'bar']);
+        static::assertStringStartsWith('doc:', $generatedHash);
+
+        $insertedKey = $index->addDocumentFromArray(['f1' => 'foo', 'f2' => 'bar'], $expectedKey);
+        static::assertSame($expectedKey, $insertedKey);
+
+        $insertedKey = $index->addDocumentFromArray(['f1' => 'foo', 'f2' => 'bar'], $expectedKey);
+        static::assertSame($expectedKey, $insertedKey);
+
+        $insertedKey = $index->addDocumentFromArray(['f1' => 'foo', 'f2' => 'bar']);
+        static::assertStringStartsNotWith('doc:', $insertedKey);
+    }
+
+    public function testDeleteDocument(): void
+    {
+        $client = $this->createMock(Client::class);
+        $client->expects(static::exactly(2))->method('executeRaw')->with('del', 'foobar')->willReturnOnConsecutiveCalls(1, 0);
+
+        $index = new Index('idx', $client, AbstractCommand::MIN_IMPLEMENTED_VERSION);
+        static::assertTrue($index->deleteDocument('foobar'));
+        static::assertFalse($index->deleteDocument('foobar'));
+    }
+
+    public function testAddField(): void
+    {
+        $client = $this->createMock(Client::class);
+        $expected = (new Alter(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setIndex('idx')->addTextField('f3');
+        $client->expects(static::exactly(2))->method('execute')->with($expected)->willReturnOnConsecutiveCalls('OK', 0);
+        $index = new Index('idx', $client, AbstractCommand::MIN_IMPLEMENTED_VERSION);
+
+        static::assertTrue($index->addField((new TextFieldOption())->setField('f3')));
+        static::assertFalse($index->addField((new TextFieldOption())->setField('f3')));
+    }
+
+    public function testDelete(): void
+    {
+        $client = $this->createMock(Client::class);
+        $normalExpectation = (new DropIndex(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setIndex('idx');
+        $withDocumentExpectation = (new DropIndex(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setIndex('idx')->setDeleteDocument();
+        $client
+            ->expects(static::exactly(3))
+            ->method('execute')
+            ->withConsecutive([$normalExpectation], [$withDocumentExpectation], [$normalExpectation])
+            ->willReturnOnConsecutiveCalls('OK', 'OK', 0)
+        ;
+        $index = new Index('idx', $client, AbstractCommand::MIN_IMPLEMENTED_VERSION);
+
+        static::assertTrue($index->delete());
+        static::assertTrue($index->delete(true));
+        static::assertFalse($index->delete());
+    }
+
+    public function testAliases(): void
+    {
+        $client = $this->createMock(Client::class);
+        $addExpectation = (new AliasAdd(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setIndex('idx')->setAlias('foo');
+        $updateExpectation = (new AliasUpdate(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setIndex('idx')->setAlias('foo');
+        $deleteExpectation = (new AliasDel(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setAlias('foo');
+        $client
+            ->expects(static::exactly(6))
+            ->method('execute')
+            ->withConsecutive([$addExpectation], [$addExpectation], [$updateExpectation], [$updateExpectation], [$deleteExpectation], [$deleteExpectation])
+            ->willReturnOnConsecutiveCalls('OK', 0, 'OK', 0, 'OK', 0)
+        ;
+        $index = new Index('idx', $client, AbstractCommand::MIN_IMPLEMENTED_VERSION);
+
+        static::assertTrue($index->addAlias('foo'));
+        static::assertFalse($index->addAlias('foo'));
+        static::assertTrue($index->updateAlias('foo'));
+        static::assertFalse($index->updateAlias('foo'));
+        static::assertTrue($index->deleteAlias('foo'));
+        static::assertFalse($index->deleteAlias('foo'));
+    }
+
+    public function testGetTagValues(): void
+    {
+        $client = $this->createMock(Client::class);
+        $expectation = (new TagVals(AbstractCommand::MIN_IMPLEMENTED_VERSION))->setIndex('idx')->setField('f1');
+        $expectedTags = ['foo', 'bar'];
+        $client
+            ->expects(static::once())
+            ->method('execute')
+            ->with($expectation)
+            ->willReturn($expectedTags)
+        ;
+        $index = new Index('idx', $client, AbstractCommand::MIN_IMPLEMENTED_VERSION);
+
+        static::assertSame($expectedTags, $index->getTagValues('f1'));
+    }
+}
diff --git a/tests/Redis/Response/InfoResponseTest.php b/tests/Redis/Response/InfoResponseTest.php
index 747bec8..04df0cf 100644
--- a/tests/Redis/Response/InfoResponseTest.php
+++ b/tests/Redis/Response/InfoResponseTest.php
@@ -286,4 +286,12 @@ public function testDataCasting(): void
         // @phpstan-ignore-next-line
         static::assertSame(1, $response->getNonExistentInt());
     }
+
+    public function testMissingIndexDefinition(): void
+    {
+        $response = new InfoResponse([]);
+        static::assertNull($response->getIndexDefinition('foobar'));
+        static::assertSame([], $response->getIndexDefinition());
+        static::assertEmpty($response->getIndexDefinition());
+    }
 }
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index f9c3929..376f8c3 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\integration;
 
 use function get_class;
+use function in_array;
 use function is_string;
 
 use Amp\Redis\Config;
@@ -273,7 +274,7 @@ public function testIntegration($clientBuilder): void
      */
     public function dataProvider(string $testName): array
     {
-        if ('testIntegration' === $testName) {
+        if (in_array($testName, ['testIntegration', 'testAddDocument'])) {
             return [
                 [static function () { return Client\PredisClient::make(new \Predis\Client(['scheme' => 'tcp', 'host' => 'localhost', 'port' => '16379', 'db' => 0])); }],
                 [static function () { return Client\RediskaClient::make(new Rediska(['servers' => [['host' => 'localhost', 'port' => '16379', 'db' => 0]]])); }],
@@ -295,4 +296,64 @@ public function dataProvider(string $testName): array
             [Client\CredisClient::class, static function () { return new Credis_Client('localhost', 16379, null, '', 0); }],
         ];
     }
+
+    /**
+     * @dataProvider dataProvider
+     * @large
+     *
+     * @param mixed $clientBuilder
+     */
+    public function testAddDocument($clientBuilder): void
+    {
+        $client = $clientBuilder();
+
+        $list = $client->execute(new IndexList());
+        static::assertEmpty($list);
+        $builder = new IndexBuilder();
+        $builder
+            ->withIndex('testDoc')
+            ->addPrefixes('person:')
+            ->addTextField('lastname', false, null, null, true)
+            ->addTextField('firstname')
+            ->addNumericField('age')
+            ->create($client)
+        ;
+        static::assertEquals(['testDoc'], $client->execute(new IndexList()));
+
+        $index = new Index('testDoc', $client);
+        $docToRemove = [];
+        $docToRemove[] = $index->addDocumentFromArray(['firstname' => 'Joe', 'lastname' => 'Doe', 'age' => 30], 'person:1');
+        $docToRemove[] = $index->addDocumentFromArray(['firstname' => 'Joe', 'age' => 30], 'person:2');
+        $docToRemove[] = $index->addDocumentFromArray(['firstname' => 'Joe', 'age' => 30, 'fullname' => 'eeeee'], 'person:3');
+
+        $search = new Search();
+
+        $query = (new Builder())
+            ->addElement(new Fuzzy('Joo'))
+        ;
+
+        $search
+            ->setIndex('testDoc')
+            ->setQuery($query->render())
+            ->setWithPayloads()
+            ->setHighlight(['lastname'], '<b>', '</b>')
+            ->setWithScores()
+        ;
+
+        /** @var PaginatedResponse $result */
+        $result = $client->execute($search);
+        static::assertCount(1, $result);
+        static::assertSame(1, $result->getPageCount());
+        static::assertSame(3, $result->getTotalCount());
+
+        /** @var array<SearchResponseItem> $page */
+        foreach ($result as $page) {
+            foreach ($page as $item) {
+                static::assertSame(30, (int) $item->getFieldValue('age'));
+            }
+        }
+
+        $client->execute((new DropIndex())->setIndex('testDoc')->setDeleteDocument());
+        $client->executeRaw('del', ...$docToRemove);
+    }
 }

From cd71df4b47d2ceb1f92760982602404e2ee5dc1b Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 5 Apr 2022 22:56:48 +0200
Subject: [PATCH 47/60] RediSearch 2.0.13 compatibility

---
 CHANGELOG.md                                  |  1 +
 src/Redis/Command/AbstractCommand.php         |  2 +-
 src/Redis/Command/Aggregate.php               | 11 +++++
 src/Redis/Command/Option/NumberedOption.php   |  4 +-
 tests/Redis/Command/AggregateTest.php         | 42 +++++++++++++++++++
 .../Command/Option/NumberedOptionTest.php     |  6 +++
 6 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 118234c..f7afd43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Documentation on paginated responses
 - Throw custom exception is client is missing
 - Add compatibility for Predis version 2.x
+- `LOAD ALL` option on Aggregate command (RediSearch `2.0.13`)
 - (dev) Add `make clean` to remove all generated files
 - (dev) Add code coverage to `\MacFJA\RediSearch\Index`
 - (dev) Add integration test for document insertion
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index ca53c69..320a9a5 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -33,7 +33,7 @@
  */
 abstract class AbstractCommand implements Command
 {
-    public const MAX_IMPLEMENTED_VERSION = '2.0.12';
+    public const MAX_IMPLEMENTED_VERSION = '2.0.13';
     public const MIN_IMPLEMENTED_VERSION = '2.0.0';
 
     /**
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 241f17f..0cccec8 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -32,6 +32,7 @@
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor;
 use MacFJA\RediSearch\Redis\Command\Option\CommandOption;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
@@ -58,6 +59,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
             'query' => new NamelessOption(null, '>=2.0.0'),
             'verbatim' => new FlagOption('VERBATIM', false, '>= 2.0.0'),
             'load' => new NumberedOption('LOAD', null, '>=2.0.0'),
+            'loadall' => CV::allowedValues(new NamedOption('LOAD', null, '>=2.0.13'), ['ALL']),
             'groupby' => [],
             'sortby' => new SortByOption(),
             'apply' => [],
@@ -157,6 +159,15 @@ public function setLoad(string ...$field): self
         return $this;
     }
 
+    public function setLoadAll(): self
+    {
+        $this->options['load']->setArguments(null);
+        $this->options['loadall']->setValue('ALL');
+        $this->lastAdded = $this->options['loadall'];
+
+        return $this;
+    }
+
     public function setWithCursor(?int $count = null, ?int $maxIdle = null): self
     {
         $this->options['cursor']
diff --git a/src/Redis/Command/Option/NumberedOption.php b/src/Redis/Command/Option/NumberedOption.php
index f52d998..088e695 100644
--- a/src/Redis/Command/Option/NumberedOption.php
+++ b/src/Redis/Command/Option/NumberedOption.php
@@ -44,9 +44,9 @@ public function __construct(string $name, ?array $arguments = null, ?string $ver
     }
 
     /**
-     * @param array<float|int|string> $arguments
+     * @param null|array<float|int|string> $arguments
      */
-    public function setArguments(array $arguments): void
+    public function setArguments(?array $arguments): void
     {
         $this->arguments = $arguments;
     }
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index 6688d0e..30f623b 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -121,6 +121,48 @@ public function testSizeOptions(): void
         static::assertSame(10, $command->getSize());
     }
 
+    public function testLoadAll(): void
+    {
+        $command = new Aggregate('2.0.12');
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setLoadAll()
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+        ], $command->getArguments());
+
+        $command = new Aggregate('2.0.13');
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setLoadAll()
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'LOAD', 'ALL',
+        ], $command->getArguments());
+
+        $command = new Aggregate('2.0.13');
+        $command
+            ->setIndex('idx')
+            ->setQuery('@text1:"hello world"')
+            ->setLoad('foo', 'bar')
+            ->setLoadAll()
+        ;
+
+        static::assertSame([
+            'idx',
+            '@text1:"hello world"',
+            'LOAD', 'ALL',
+        ], $command->getArguments());
+    }
+
     public function testFullOption(): void
     {
         $command = new Aggregate();
diff --git a/tests/Redis/Command/Option/NumberedOptionTest.php b/tests/Redis/Command/Option/NumberedOptionTest.php
index 545c50a..58abe71 100644
--- a/tests/Redis/Command/Option/NumberedOptionTest.php
+++ b/tests/Redis/Command/Option/NumberedOptionTest.php
@@ -50,6 +50,12 @@ public function testGetSetArguments(): void
         $option = new NumberedOption('FOO', ['foo']);
         static::assertIsArray($option->getArguments());
         static::assertEquals(['foo'], $option->getArguments());
+
+        $option = new NumberedOption('FOO', ['foo']);
+        $option->setArguments(null);
+        static::assertNull($option->getArguments());
+        static::assertFalse($option->isValid());
+        static::assertSame([], $option->render());
     }
 
     public function testGetName(): void

From 6a58bf05a374634bb9156ac9cd8ee61010a1902f Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Tue, 5 Apr 2022 23:17:34 +0200
Subject: [PATCH 48/60] RediSearch 2.2.0 compatibility

---
 CHANGELOG.md                                  |   2 +
 src/Redis/Command/AbstractCommand.php         |   2 +-
 src/Redis/Command/Profile.php                 |  92 +++++++++++++
 .../Command/ProfileCommand/QueryOption.php    |  79 +++++++++++
 src/Redis/Command/Search.php                  |   7 +-
 tests/Redis/Command/ProfileTest.php           | 125 ++++++++++++++++++
 tests/Redis/Command/SearchTest.php            |   7 +
 7 files changed, 312 insertions(+), 2 deletions(-)
 create mode 100644 src/Redis/Command/Profile.php
 create mode 100644 src/Redis/Command/ProfileCommand/QueryOption.php
 create mode 100644 tests/Redis/Command/ProfileTest.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7afd43..e2a3bf7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Throw custom exception is client is missing
 - Add compatibility for Predis version 2.x
 - `LOAD ALL` option on Aggregate command (RediSearch `2.0.13`)
+- Profile command (RediSearch `2.2.0`) ([Issue#4])
 - (dev) Add `make clean` to remove all generated files
 - (dev) Add code coverage to `\MacFJA\RediSearch\Index`
 - (dev) Add integration test for document insertion
@@ -223,6 +224,7 @@ First version
 [1.0.0]: https://github.com/MacFJA/php-redisearch/releases/tag/1.0.0
 
 [Issue#2]: https://github.com/MacFJA/php-redisearch/issues/2
+[Issue#4]: https://github.com/MacFJA/php-redisearch/issues/4
 [Issue#5]: https://github.com/MacFJA/php-redisearch/issues/5
 [Issue#6]: https://github.com/MacFJA/php-redisearch/issues/6
 [Issue#9]: https://github.com/MacFJA/php-redisearch/issues/9
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index 320a9a5..ca99b6b 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -33,7 +33,7 @@
  */
 abstract class AbstractCommand implements Command
 {
-    public const MAX_IMPLEMENTED_VERSION = '2.0.13';
+    public const MAX_IMPLEMENTED_VERSION = '2.2.0';
     public const MIN_IMPLEMENTED_VERSION = '2.0.0';
 
     /**
diff --git a/src/Redis/Command/Profile.php b/src/Redis/Command/Profile.php
new file mode 100644
index 0000000..5dbd44c
--- /dev/null
+++ b/src/Redis/Command/Profile.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\ProfileCommand\QueryOption;
+
+class Profile extends AbstractCommand
+{
+    public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VERSION)
+    {
+        parent::__construct([
+            'index' => new NamelessOption(null, '>=2.2.0'),
+            'type' => CV::allowedValues(new NamelessOption(null, '>=2.2.0'), ['SEARCH', 'AGGREGATE']),
+            'limited' => new FlagOption('LIMITED', false, '>=2.2.0'),
+            'query' => new QueryOption('>=2.2.0'),
+        ], $rediSearchVersion);
+    }
+
+    public function setIndex(string $index): self
+    {
+        $this->options['index']->setValue($index);
+
+        return $this;
+    }
+
+    /**
+     * @param Aggregate|Search $query
+     */
+    public function setQuery($query): self
+    {
+        /** @var QueryOption $queryOption */
+        $queryOption = $this->options['query'];
+        $queryOption->setCommand($query);
+        $this->options['type']->setValue($queryOption->isSearch() ? 'SEARCH' : 'AGGREGATE');
+        $this->options['index']->setValue($queryOption->getIndex());
+
+        return $this;
+    }
+
+    public function setTypeAggregate(): self
+    {
+        $this->options['type']->setValue('AGGREGATE');
+
+        return $this;
+    }
+
+    public function setTypeSearch(): self
+    {
+        $this->options['type']->setValue('SEARCH');
+
+        return $this;
+    }
+
+    public function setLimited(bool $active = true): self
+    {
+        $this->options['limited']->setActive($active);
+
+        return $this;
+    }
+
+    public function getId(): string
+    {
+        return 'FT.PROFILE';
+    }
+
+    protected function getRequiredOptions(): array
+    {
+        return ['index', 'type', 'query'];
+    }
+}
diff --git a/src/Redis/Command/ProfileCommand/QueryOption.php b/src/Redis/Command/ProfileCommand/QueryOption.php
new file mode 100644
index 0000000..3865ddb
--- /dev/null
+++ b/src/Redis/Command/ProfileCommand/QueryOption.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\ProfileCommand;
+
+use function assert;
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
+use MacFJA\RediSearch\Redis\Command\Search;
+
+class QueryOption extends AbstractCommandOption
+{
+    /**
+     * @var null|Aggregate|Search
+     */
+    private $command;
+
+    /**
+     * @param Aggregate|Search $command
+     */
+    public function setCommand($command): self
+    {
+        $this->command = $command;
+
+        return $this;
+    }
+
+    public function isValid(): bool
+    {
+        return $this->command instanceof Search || $this->command instanceof Aggregate;
+    }
+
+    public function getOptionData()
+    {
+        return [
+            'command' => $this->command,
+        ];
+    }
+
+    public function isSearch(): bool
+    {
+        return $this->command instanceof Search;
+    }
+
+    public function getIndex(): string
+    {
+        assert($this->command instanceof Search || $this->command instanceof Aggregate);
+
+        return $this->command->getIndex();
+    }
+
+    protected function doRender(?string $version): array
+    {
+        assert($this->command instanceof Search || $this->command instanceof Aggregate);
+        $arguments = $this->command->getArguments();
+        // remove index
+        array_shift($arguments);
+
+        return array_merge(['QUERY'], $arguments);
+    }
+}
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 9cf4fab..1d2e979 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -42,7 +42,6 @@
 use MacFJA\RediSearch\Redis\Response\SearchResponseItem;
 
 /**
- * @method PaginatedResponse<SearchResponseItem> parseResponse(mixed $data)
  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  */
 class Search extends AbstractCommand implements PaginatedCommand
@@ -90,6 +89,11 @@ public function setIndex(string $index): self
         return $this;
     }
 
+    public function getIndex(): string
+    {
+        return $this->options['index']->getValue();
+    }
+
     public function setQuery(string $query): self
     {
         $this->options['query']->setValue($query);
@@ -306,6 +310,7 @@ public function getSize(): ?int
      * @param mixed $data
      *
      * @return PaginatedResponse
+     * @psalm-return PaginatedResponse<SearchResponseItem>
      *
      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
      * @SuppressWarnings(PHPMD.NPathComplexity)
diff --git a/tests/Redis/Command/ProfileTest.php b/tests/Redis/Command/ProfileTest.php
new file mode 100644
index 0000000..a7b985e
--- /dev/null
+++ b/tests/Redis/Command/ProfileTest.php
@@ -0,0 +1,125 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command;
+
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\Profile;
+use MacFJA\RediSearch\Redis\Command\Search;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ *
+ * @covers \MacFJA\RediSearch\Redis\Command\Profile
+ * @covers \MacFJA\RediSearch\Redis\Command\ProfileCommand\QueryOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Search
+ * @uses \MacFJA\RediSearch\Redis\Command\Aggregate
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor
+ *
+ * @internal
+ */
+class ProfileTest extends TestCase
+{
+    public function testGetId(): void
+    {
+        $command = new Profile();
+        static::assertSame('FT.PROFILE', $command->getId());
+    }
+
+    public function testFullOption(): void
+    {
+        $aggregate = new Aggregate(Profile::MAX_IMPLEMENTED_VERSION);
+        $aggregate->setQuery('@foo:bar')->setIndex('idx');
+        $command = new Profile(Profile::MAX_IMPLEMENTED_VERSION);
+        $command
+            ->setIndex('idx')
+            ->setTypeAggregate()
+            ->setLimited()
+            ->setQuery($aggregate)
+            ;
+
+        static::assertSame([
+            'idx', 'AGGREGATE', 'LIMITED', 'QUERY', '@foo:bar',
+        ], $command->getArguments());
+    }
+
+    public function testSearch(): void
+    {
+        $search = new Search('2.2.0');
+        $search->setIndex('idx')->setQuery('@foo:bar');
+        $command = new Profile('2.2.0');
+        $command
+            ->setIndex('idx')
+            ->setTypeSearch()
+            ->setQuery($search)
+        ;
+
+        static::assertSame([
+            'idx', 'SEARCH', 'QUERY', '@foo:bar',
+        ], $command->getArguments());
+    }
+
+    public function testAutoFill(): void
+    {
+        $search = new Search('2.2.0');
+        $search->setIndex('idx')->setQuery('@foo:bar');
+        $command = new Profile('2.2.0');
+        $command
+            ->setQuery($search)
+        ;
+
+        static::assertSame([
+            'idx', 'SEARCH', 'QUERY', '@foo:bar',
+        ], $command->getArguments());
+    }
+
+    public function testOverride(): void
+    {
+        $search = new Search('2.2.0');
+        $search->setIndex('idx')->setQuery('@foo:bar');
+        $command = new Profile('2.2.0');
+        $command
+            ->setQuery($search)
+            ->setIndex('idx2')
+            ->setTypeAggregate()
+        ;
+
+        static::assertSame([
+            'idx2', 'AGGREGATE', 'QUERY', '@foo:bar',
+        ], $command->getArguments());
+    }
+}
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index 6fc6e5c..ad15e84 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -63,6 +63,13 @@ public function testGetId(): void
         static::assertSame('FT.SEARCH', $command->getId());
     }
 
+    public function testGetIndex(): void
+    {
+        $command = new Search();
+        $command->setIndex('idx');
+        static::assertSame('idx', $command->getIndex());
+    }
+
     public function testSizeOptions(): void
     {
         $command = new Search();

From c7c25cf28b9444c8a795dde22fdaebf5aaf2fe41 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Jul 2022 22:21:52 +0200
Subject: [PATCH 49/60] RediSearch 2.4.2 compatibility

---
 CHANGELOG.md                                  |   1 +
 src/Query/Builder/QueryElementVector.php      | 149 +++++++++++
 src/Redis/Command/AbstractCommand.php         |   2 +-
 src/Redis/Command/AddFieldOptionTrait.php     |  24 ++
 .../CreateCommand/VectorFieldOption.php       | 175 ++++++++++++
 src/Redis/Command/Option/OptionListOption.php |  67 +++++
 src/Redis/Command/Search.php                  |  17 ++
 src/Redis/Response/SpellCheckResponseItem.php |   3 +
 tests/IndexBuilderTest.php                    |   6 +
 tests/Query/Builder/IncludeSpaceTest.php      |   3 +
 tests/Query/Builder/PriorityTest.php          |   3 +
 .../Query/Builder/QueryElementVectorTest.php  | 119 +++++++++
 tests/Query/BuilderTest.php                   | 249 ++++++++++++++++++
 .../CreateCommand/VectorFieldOptionTest.php   | 176 +++++++++++++
 tests/Redis/Command/CreateTest.php            |  12 +
 tests/Redis/Command/IndexListTest.php         |   2 +-
 .../Command/Option/OptionListOptionTest.php   | 126 +++++++++
 tests/Redis/Command/SearchTest.php            |  27 +-
 18 files changed, 1158 insertions(+), 3 deletions(-)
 create mode 100644 src/Query/Builder/QueryElementVector.php
 create mode 100644 src/Redis/Command/CreateCommand/VectorFieldOption.php
 create mode 100644 src/Redis/Command/Option/OptionListOption.php
 create mode 100644 tests/Query/Builder/QueryElementVectorTest.php
 create mode 100644 tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php
 create mode 100644 tests/Redis/Command/Option/OptionListOptionTest.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2a3bf7..0a3e8c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Add compatibility for Predis version 2.x
 - `LOAD ALL` option on Aggregate command (RediSearch `2.0.13`)
 - Profile command (RediSearch `2.2.0`) ([Issue#4])
+- Vector feature (RediSearch `2.4.0` / `2.4.2`)
 - (dev) Add `make clean` to remove all generated files
 - (dev) Add code coverage to `\MacFJA\RediSearch\Index`
 - (dev) Add integration test for document insertion
diff --git a/src/Query/Builder/QueryElementVector.php b/src/Query/Builder/QueryElementVector.php
new file mode 100644
index 0000000..c24c055
--- /dev/null
+++ b/src/Query/Builder/QueryElementVector.php
@@ -0,0 +1,149 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Query\Builder;
+
+use function array_key_exists;
+use function count;
+use function gettype;
+use InvalidArgumentException;
+use function is_int;
+use function is_string;
+use function strlen;
+
+class QueryElementVector implements QueryElementDecorator
+{
+    private const PARAMETERS = [
+        'EF_RUNTIME' => 'int',
+    ];
+
+    /** @var QueryElement */
+    private $element;
+
+    /** @var int|string */
+    private $number;
+
+    /** @var string */
+    private $field;
+
+    /** @var string */
+    private $blob;
+
+    /** @var null|string */
+    private $scoreAlias;
+
+    /** @var array<string,int|string> */
+    private $parameters = [];
+
+    /**
+     * @param int|scalar|string $number
+     */
+    public function __construct(QueryElement $element, $number, string $field, string $blob, ?string $scoreAlias = null)
+    {
+        if (!is_string($number) && !is_int($number)) {
+            throw new InvalidArgumentException();
+        }
+        $this->element = $element;
+        $this->number = $number;
+        $this->field = $field;
+        $this->blob = $blob;
+        $this->scoreAlias = $scoreAlias;
+    }
+
+    public function render(?callable $escaper = null): string
+    {
+        return sprintf(
+            '(%s)=>[KNN %s @%s $%s%s%s]',
+            $this->element->render($escaper),
+            $this->getRenderedNumber(),
+            $this->getRenderedField(),
+            $this->getRenderedBlob(),
+            $this->getRenderedAttributes(),
+            $this->getRenderedAlias()
+        );
+    }
+
+    /**
+     * @param int|string $value
+     *
+     * @return $this
+     */
+    public function addParameter(string $name, $value): self
+    {
+        if (!array_key_exists($name, self::PARAMETERS)) {
+            throw new InvalidArgumentException();
+        }
+        if (
+            !(gettype($value) === self::PARAMETERS[$name])
+            && (is_string($value) && (strlen($value) < 2 || !(0 === strpos($value, '$'))))
+        ) {
+            throw new InvalidArgumentException();
+        }
+        $this->parameters[$name] = $value;
+
+        return $this;
+    }
+
+    public function priority(): int
+    {
+        return self::PRIORITY_BEFORE;
+    }
+
+    public function includeSpace(): bool
+    {
+        return false;
+    }
+
+    private function getRenderedNumber(): string
+    {
+        return is_string($this->number) ? ('$'.ltrim($this->number, '$')) : ((string) $this->number);
+    }
+
+    private function getRenderedField(): string
+    {
+        return ltrim($this->field, '@');
+    }
+
+    private function getRenderedBlob(): string
+    {
+        return ltrim($this->blob, '$');
+    }
+
+    private function getRenderedAlias(): string
+    {
+        if (!is_string($this->scoreAlias)) {
+            return '';
+        }
+
+        return ' AS '.$this->scoreAlias;
+    }
+
+    private function getRenderedAttributes(): string
+    {
+        if (0 === count($this->parameters)) {
+            return '';
+        }
+
+        return ' '.implode(' ', array_map(static function ($key, $value) {
+            return $key.' '.$value;
+        }, array_keys($this->parameters), array_values($this->parameters)));
+    }
+}
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index ca99b6b..695d7f7 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -33,7 +33,7 @@
  */
 abstract class AbstractCommand implements Command
 {
-    public const MAX_IMPLEMENTED_VERSION = '2.2.0';
+    public const MAX_IMPLEMENTED_VERSION = '2.4.0';
     public const MIN_IMPLEMENTED_VERSION = '2.0.0';
 
     /**
diff --git a/src/Redis/Command/AddFieldOptionTrait.php b/src/Redis/Command/AddFieldOptionTrait.php
index ced4117..db0fadb 100644
--- a/src/Redis/Command/AddFieldOptionTrait.php
+++ b/src/Redis/Command/AddFieldOptionTrait.php
@@ -30,6 +30,7 @@
 use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption;
 
 trait AddFieldOptionTrait
 {
@@ -77,6 +78,17 @@ public function addTagField(string $name, ?string $separator = null, bool $sorta
         );
     }
 
+    public function addVectorField(string $name, string $algorithm, string $type, int $dimension, string $distanceMetric, bool $noIndex = false): self
+    {
+        return $this->addField(
+            (new VectorFieldOption())
+                ->setField($name)
+                ->setAlgorithm($algorithm)
+                ->addAttribute($type, $dimension, $distanceMetric)
+                ->setNoIndex($noIndex)
+        );
+    }
+
     public function addJSONTextField(string $path, string $attribute, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false): self
     {
         return $this->addJSONField(
@@ -125,6 +137,18 @@ public function addJSONTagField(string $path, string $attribute, ?string $separa
         );
     }
 
+    public function addJSONVectorField(string $path, string $attribute, string $algorithm, string $type, int $dimension, string $distanceMetric, bool $noIndex = false): self
+    {
+        return $this->addJSONField(
+            $path,
+            (new VectorFieldOption())
+                ->setField($attribute)
+                ->setAlgorithm($algorithm)
+                ->addAttribute($type, $dimension, $distanceMetric)
+                ->setNoIndex($noIndex)
+        );
+    }
+
     public function addField(CreateCommandFieldOption $option): self
     {
         if (!($this instanceof AbstractCommand)) {
diff --git a/src/Redis/Command/CreateCommand/VectorFieldOption.php b/src/Redis/Command/CreateCommand/VectorFieldOption.php
new file mode 100644
index 0000000..98ea81b
--- /dev/null
+++ b/src/Redis/Command/CreateCommand/VectorFieldOption.php
@@ -0,0 +1,175 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
+
+use function in_array;
+use InvalidArgumentException;
+use function is_int;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\OptionListOption;
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+use OutOfRangeException;
+use RuntimeException;
+
+/**
+ * @method VectorFieldOption setField(string $name)
+ * @method VectorFieldOption setNoIndex(bool $active)
+ * @method VectorFieldOption setAlgorithm(string $algorithm)
+ */
+class VectorFieldOption extends GroupedOption implements CreateCommandFieldOption
+{
+    use BaseCreateFieldOptionTrait;
+    use WithPublicGroupedSetterTrait;
+
+    public const ALGORITHM_FLAT = 'FLAT';
+    public const ALGORITHM_HNSW = 'HNSW';
+    public const TYPE_FLOAT32 = 'FLOAT32';
+    public const DISTANCE_METRIC_L2 = 'L2';
+    public const DISTANCE_METRIC_IP = 'IP';
+    public const DISTANCE_METRIC_COSINE = 'COSINE';
+    private const TYPES = [self::TYPE_FLOAT32];
+    private const DISTANCE_METRICS = [self::DISTANCE_METRIC_IP, self::DISTANCE_METRIC_COSINE, self::DISTANCE_METRIC_L2];
+
+    public function __construct()
+    {
+        parent::__construct($this->getConstructorOptions('VECTOR', [
+            'algorithm' => CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.4.0'), [self::ALGORITHM_FLAT, self::ALGORITHM_HNSW]),
+            'count' => CustomValidatorOption::isNumeric(new NamelessOption(null, '>=2.4.0')),
+            'attributes' => new OptionListOption('>=2.4.0'),
+        ]), ['field', 'algorithm', 'count', 'type'], ['type'], '>=2.4.0');
+    }
+
+    public function getFieldName(): string
+    {
+        return $this->getDataOfOption('field');
+    }
+
+    public function addFlatAttribute(string $type, int $dimension, string $distanceMetric, ?int $initialCapacity = null, ?int $blockSize = null): self
+    {
+        if (!(self::ALGORITHM_FLAT === $this->getDataOfOption('algorithm'))) {
+            throw new RuntimeException();
+        }
+
+        return $this->addAttribute($type, $dimension, $distanceMetric, $initialCapacity, $blockSize);
+    }
+
+    public function addHnswAttribute(string $type, int $dimension, string $distanceMetric, ?int $initialCapacity = null, ?int $maxEdge = null, ?int $efConstruction = null, ?int $efRuntime = null): self
+    {
+        if (!(self::ALGORITHM_HNSW === $this->getDataOfOption('algorithm'))) {
+            throw new RuntimeException();
+        }
+
+        return $this->addAttribute($type, $dimension, $distanceMetric, $initialCapacity, null, $maxEdge, $efConstruction, $efRuntime);
+    }
+
+    public function addAttribute(string $type, int $dimension, string $distanceMetric, ?int $initialCapacity = null, ?int $blockSize = null, ?int $maxEdge = null, ?int $efConstruction = null, ?int $efRuntime = null): self
+    {
+        $algorithm = $this->getDataOfOption('algorithm');
+
+        if (self::ALGORITHM_FLAT === $algorithm) {
+            $efRuntime = null;
+            $efConstruction = null;
+            $maxEdge = null;
+        }
+        if (self::ALGORITHM_HNSW === $algorithm) {
+            $blockSize = null;
+        }
+
+        $this->validateArguments($type, $dimension, $distanceMetric, $initialCapacity, $blockSize, $maxEdge, $efConstruction, $efRuntime);
+
+        $attribute = new GroupedOption([
+            'type' => new NamedOption('TYPE', $type, '>=2.4.0'),
+            'dim' => new NamedOption('DIM', $dimension, '>=2.4.0'),
+            'distance_metric' => new NamedOption('DISTANCE_METRIC', $distanceMetric, '>=2.4.0'),
+            'initial_cap' => new NamedOption('INITIAL_CAP', $initialCapacity, '>=2.4.0'),
+            'block_size' => new NamedOption('BLOCK_SIZE', $blockSize, '>=2.4.0'),
+            'm' => new NamedOption('M', $maxEdge, '>=2.4.0'),
+            'ef_construction' => new NamedOption('EF_CONSTRUCTION', $efConstruction, '>=2.4.0'),
+            'ef_runtime' => new NamedOption('EF_RUNTIME', $efRuntime, '>=2.4.0'),
+        ], ['type', 'dim', 'distance_metric'], ['type', 'dim', 'distance_metric', 'initial_cap', 'block_size', 'm', 'ef_construction', 'ef_runtime']);
+
+        /** @var array<GroupedOption> $options */
+        $options = $this->getDataOfOption('attributes');
+        $options[] = $attribute;
+        $this->setDataOfOption('attributes', ...$options);
+
+        $countAddition = 6
+            + (is_int($initialCapacity) ? 2 : 0)
+            + (is_int($blockSize) ? 2 : 0)
+            + (is_int($maxEdge) ? 2 : 0)
+            + (is_int($efConstruction) ? 2 : 0)
+            + (is_int($efRuntime) ? 2 : 0)
+        ;
+
+        /** @var int $count */
+        $count = $this->getDataOfOption('count') ?? 0;
+        $count += $countAddition;
+        $this->setDataOfOption('count', $count);
+
+        return $this;
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function publicSetter(): array
+    {
+        return ['field', 'algorithm', 'no_index'];
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter) -- Dynamic variable name
+     */
+    private function validateArguments(string $type, int $dimension, string $distanceMetric, ?int $initialCapacity = null, ?int $blockSize = null, ?int $maxEdge = null, ?int $efConstruction = null, ?int $efRuntime = null): void
+    {
+        $atLeastOne = ['dimension', 'initialCapacity', 'blockSize', 'efConstruction', 'efRuntime'];
+
+        if (!in_array($type, self::TYPES, true)) {
+            throw new InvalidArgumentException(sprintf(
+                '"%s" is not a known type (expect one of %s)',
+                $type,
+                implode(', ', self::TYPES)
+            ));
+        }
+
+        foreach ($atLeastOne as $varName) {
+            if (is_int(${$varName}) && ${$varName} < 1) {
+                throw new OutOfRangeException(sprintf('$%s must be greater than 0', $varName));
+            }
+        }
+
+        if (!in_array($distanceMetric, self::DISTANCE_METRICS, true)) {
+            throw new InvalidArgumentException(sprintf(
+                '"%s" is not a known distance metric (expect one of %s)',
+                $distanceMetric,
+                implode(', ', self::DISTANCE_METRICS)
+            ));
+        }
+
+        if (is_int($maxEdge) && $maxEdge < 0) {
+            throw new OutOfRangeException('$maxEdge must be a positive number');
+        }
+    }
+}
diff --git a/src/Redis/Command/Option/OptionListOption.php b/src/Redis/Command/Option/OptionListOption.php
new file mode 100644
index 0000000..7a1f8c2
--- /dev/null
+++ b/src/Redis/Command/Option/OptionListOption.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Redis\Command\Option;
+
+use function is_bool;
+use function is_scalar;
+
+class OptionListOption extends AbstractCommandOption
+{
+    /** @var array<CommandOption> */
+    private $options = [];
+
+    public function addOption(CommandOption $option): self
+    {
+        $this->options[] = $option;
+
+        return $this;
+    }
+
+    public function setValue(CommandOption ...$options): self
+    {
+        $this->options = $options;
+
+        return $this;
+    }
+
+    public function isValid(): bool
+    {
+        return true;
+    }
+
+    public function getOptionData()
+    {
+        return $this->options;
+    }
+
+    protected function doRender(?string $version): array
+    {
+        $arguments = array_filter($this->options, static function (CommandOption $option) use ($version) {
+            return $option->isCompatible($version) && $option->isValid();
+        });
+        $arguments = array_reduce($arguments, static function ($carry, CommandOption $option) use ($version) {
+            return array_merge($carry, $option->render($version));
+        }, []);
+
+        return array_filter($arguments, static function ($item) { return !is_bool($item) && is_scalar($item); });
+    }
+}
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 1d2e979..3b6f6fa 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -30,6 +30,7 @@
 use MacFJA\RediSearch\Redis\Command\Option\FlagOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption;
 use MacFJA\RediSearch\Redis\Command\Option\NumberedOption;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
@@ -75,6 +76,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
                 'scorer' => new NamedOption('SCORER', null, '>=2.0.0'),
                 'explainscore' => new FlagOption('EXPLAINSCORE', false, '>=2.0.0'),
                 'payload' => new NamedOption('PAYLOAD', null, '>=2.0.0'),
+                'params' => new NotEmptyOption(new NumberedOption('PARAMS', null, '>=2.4.0')),
                 'sortby' => new SortByOption(),
                 'limit' => new LimitOption(),
             ],
@@ -285,6 +287,21 @@ public function setReturn(string ...$field): self
         return $this;
     }
 
+    /**
+     * @param int|scalar|string $value
+     *
+     * @return $this
+     */
+    public function addParam(string $name, $value): self
+    {
+        $params = $this->options['params']->getArguments() ?? [];
+        $params[] = $name;
+        $params[] = (string) $value;
+        $this->options['params']->setArguments($params);
+
+        return $this;
+    }
+
     public function getId(): string
     {
         return 'FT.SEARCH';
diff --git a/src/Redis/Response/SpellCheckResponseItem.php b/src/Redis/Response/SpellCheckResponseItem.php
index bd2c464..316259a 100644
--- a/src/Redis/Response/SpellCheckResponseItem.php
+++ b/src/Redis/Response/SpellCheckResponseItem.php
@@ -56,6 +56,9 @@ public function getSuggestions(bool $normalized = false): array
         if (false === $normalized) {
             return $this->suggestions;
         }
+        if (0 === count($this->suggestions)) {
+            return [];
+        }
         $highest = max($this->suggestions);
 
         if ($highest <= 0) {
diff --git a/tests/IndexBuilderTest.php b/tests/IndexBuilderTest.php
index 96a98bb..bcff32a 100644
--- a/tests/IndexBuilderTest.php
+++ b/tests/IndexBuilderTest.php
@@ -27,6 +27,7 @@
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Command\Create;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption;
 use PHPUnit\Framework\TestCase;
 use RuntimeException;
 
@@ -43,12 +44,14 @@
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\OptionListOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\BaseCreateFieldOptionTrait
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption
+ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
  * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait
@@ -289,6 +292,9 @@ public function testAllJsonAdd(): void
 
         $builder->addJSONTagField('$.city.languages', 'languages');
         $createCommand->addJSONTagField('$.city.languages', 'languages');
+
+        $builder->addJSONVectorField('$.city.vec', 'vec', VectorFieldOption::ALGORITHM_FLAT, VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_L2);
+        $createCommand->addJSONVectorField('$.city.vec', 'vec', VectorFieldOption::ALGORITHM_FLAT, VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_L2);
         static::assertEquals($createCommand, $builder->getCommand());
     }
 
diff --git a/tests/Query/Builder/IncludeSpaceTest.php b/tests/Query/Builder/IncludeSpaceTest.php
index 4dc02de..5c5e252 100644
--- a/tests/Query/Builder/IncludeSpaceTest.php
+++ b/tests/Query/Builder/IncludeSpaceTest.php
@@ -59,6 +59,7 @@
  * @covers \MacFJA\RediSearch\Query\Builder\Prefix
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElement::priority
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementVector
  * @covers \MacFJA\RediSearch\Query\Builder\RawElement
  * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
  * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
@@ -124,6 +125,7 @@ public function testModifierIncludeSpace(): void
         $optional = new Optional(new RawElement('foo bar'));
         $prefix = new Prefix('foo bar');
         $attribute = new QueryElementAttribute(new RawElement('foo bar'));
+        $vector = new Builder\QueryElementVector(new RawElement('foo bar'), 10, 'vec', 'BLOB');
         $raw = new RawElement('foo bar');
         $word = new Word('foo bar');
 
@@ -133,6 +135,7 @@ public function testModifierIncludeSpace(): void
         static::assertFalse($optional->includeSpace());
         static::assertFalse($prefix->includeSpace());
         static::assertTrue($attribute->includeSpace());
+        static::assertFalse($vector->includeSpace());
         static::assertTrue($raw->includeSpace());
         static::assertTrue($word->includeSpace());
     }
diff --git a/tests/Query/Builder/PriorityTest.php b/tests/Query/Builder/PriorityTest.php
index 4d6f4cc..18e8bb3 100644
--- a/tests/Query/Builder/PriorityTest.php
+++ b/tests/Query/Builder/PriorityTest.php
@@ -60,6 +60,7 @@
  * @covers \MacFJA\RediSearch\Query\Builder\Prefix
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElement::priority
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementVector
  * @covers \MacFJA\RediSearch\Query\Builder\RawElement
  * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
  * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
@@ -107,6 +108,7 @@ public function testModifierPriority(): void
         $optional = new Optional(new RawElement('foo'));
         $prefix = new Prefix('foo');
         $attribute = new QueryElementAttribute(new RawElement('foo'));
+        $vector = new Builder\QueryElementVector(new RawElement('foo'), 10, 'vec', 'BLOB');
         $raw = new RawElement('foo');
         $word = new Word('foo');
 
@@ -116,6 +118,7 @@ public function testModifierPriority(): void
         static::assertSame(QueryElement::PRIORITY_NORMAL, $optional->priority());
         static::assertSame(QueryElement::PRIORITY_NORMAL, $prefix->priority());
         static::assertSame(QueryElement::PRIORITY_NORMAL, $attribute->priority());
+        static::assertSame(QueryElement::PRIORITY_BEFORE, $vector->priority());
         static::assertSame(QueryElement::PRIORITY_NORMAL, $raw->priority());
         static::assertSame(QueryElement::PRIORITY_NORMAL, $word->priority());
     }
diff --git a/tests/Query/Builder/QueryElementVectorTest.php b/tests/Query/Builder/QueryElementVectorTest.php
new file mode 100644
index 0000000..25c5d9d
--- /dev/null
+++ b/tests/Query/Builder/QueryElementVectorTest.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Query\Builder;
+
+use InvalidArgumentException;
+use MacFJA\RediSearch\Query\Builder;
+use MacFJA\RediSearch\Redis\Command\Search;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementVector
+ *
+ * @uses \MacFJA\RediSearch\Query\Builder
+ * @uses \MacFJA\RediSearch\Query\Builder\AbstractGroup
+ * @uses \MacFJA\RediSearch\Query\Builder\AndGroup
+ * @uses \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
+ * @uses \MacFJA\RediSearch\Query\Builder\ExactMatch
+ * @uses \MacFJA\RediSearch\Query\Builder\FieldFacet
+ * @uses \MacFJA\RediSearch\Query\Builder\Optional
+ * @uses \MacFJA\RediSearch\Query\Builder\OrGroup
+ * @uses \MacFJA\RediSearch\Query\Builder\RawElement
+ * @uses \MacFJA\RediSearch\Query\Builder\TagFacet
+ * @uses \MacFJA\RediSearch\Query\Builder\Word
+ * @uses \MacFJA\RediSearch\Query\Escaper
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\LanguageOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Search
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
+ *
+ * @internal
+ */
+class QueryElementVectorTest extends TestCase
+{
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample(): void
+    {
+        // $expected = 'idx "(@type:{shirt} ~@color:{blue})=>[KNN $K @vec $BLOB AS my_scores]" PARAMS 4 BLOB "\12\a9\f5\6c" K 10 SORTBY my_scores';
+        // Data escaping is partially handled by the Redis lib
+        $expected = 'idx (@type:{shirt} ~@color:{blue})=>[KNN $K @vec $BLOB AS my_scores] PARAMS 4 BLOB \12\a9\f5\6c K 10 SORTBY my_scores';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $group = new Builder\AndGroup([
+            new Builder\TagFacet(['type'], 'shirt'),
+            new Builder\Optional(new Builder\TagFacet(['color'], 'blue')),
+        ]);
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector($group, 'K', 'vec', 'BLOB', 'my_scores')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->addParam('K', 10)
+            ->setSortBy('my_scores')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    public function testInvalidType(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $element = new Builder\RawElement('*');
+        new Builder\QueryElementVector($element, false, 'vec', 'BLOB');
+    }
+
+    public function testInvalidParameterName(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $element = new Builder\RawElement('*');
+        $vector = new Builder\QueryElementVector($element, 10, 'vec', 'BLOB');
+        $vector->addParameter('foo', 'bar');
+    }
+
+    public function testInvalidParameterValue(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $element = new Builder\RawElement('*');
+        $vector = new Builder\QueryElementVector($element, 10, 'vec', 'BLOB');
+        $vector->addParameter('EF_RUNTIME', 'bar');
+    }
+}
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index 6c23831..e1ec1d7 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -21,7 +21,9 @@
 
 namespace MacFJA\RediSearch\tests\Query;
 
+use InvalidArgumentException;
 use MacFJA\RediSearch\Query\Builder;
+use MacFJA\RediSearch\Redis\Command\Search;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -39,12 +41,33 @@
  * @covers \MacFJA\RediSearch\Query\Builder\OrGroup
  * @covers \MacFJA\RediSearch\Query\Builder\Prefix
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
+ * @covers \MacFJA\RediSearch\Query\Builder\QueryElementVector
  * @covers \MacFJA\RediSearch\Query\Builder\RawElement
  * @covers \MacFJA\RediSearch\Query\Builder\TagFacet
  * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
  * @covers \MacFJA\RediSearch\Query\Builder\Word
  * @covers \MacFJA\RediSearch\Query\Escaper
  *
+ * @covers \MacFJA\RediSearch\Redis\Command\Search::addParam
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\LanguageOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Search
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
+ *
  * @internal
  */
 class BuilderTest extends TestCase
@@ -521,4 +544,230 @@ public function testDocExample13(): void
 
         static::assertSame($expected, $builder->render());
     }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample1(): void
+    {
+        // $expected = 'idx "*=>[KNN 100 @vec $BLOB]" PARAMS 2 BLOB "\12\a9\f5\6c"';
+        // Data escaping is partially handled by the Redis lib, + this lib put extra safety ()
+        $expected = 'idx (*)=>[KNN 100 @vec $BLOB] PARAMS 2 BLOB \12\a9\f5\6c';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector(new Builder\RawElement('*'), 100, 'vec', 'BLOB')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample12(): void
+    {
+        // $expected = 'idx "*=>[KNN 100 @vec $BLOB]" PARAMS 2 BLOB "\12\a9\f5\6c"';
+        // Data escaping is partially handled by the Redis lib, + this lib put extra safety ()
+        $expected = 'idx (*)=>[KNN 100 @vec $BLOB] PARAMS 2 BLOB \12\a9\f5\6c';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector(new Builder\RawElement('*'), 100, '@vec', '$BLOB')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample2(): void
+    {
+        // $expected = 'idx "*=>[KNN 100 @vec $BLOB]" PARAMS 2 BLOB "\12\a9\f5\6c" SORTBY __vec_score';
+        // Data escaping is partially handled by the Redis lib, + this lib put extra safety ()
+        $expected = 'idx (*)=>[KNN 100 @vec $BLOB] PARAMS 2 BLOB \12\a9\f5\6c SORTBY __vec_score';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector(new Builder\RawElement('*'), 100, 'vec', 'BLOB')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->setSortBy('__vec_score')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample3(): void
+    {
+        // $expected = 'idx "*=>[KNN $K @vec $BLOB EF_RUNTIME $EF]" PARAMS 6 BLOB "\12\a9\f5\6c" K 10 EF 150';
+        // Data escaping is partially handled by the Redis lib, + this lib put extra safety ()
+        $expected = 'idx (*)=>[KNN $K @vec $BLOB EF_RUNTIME $EF] PARAMS 6 BLOB \12\a9\f5\6c K 10 EF 150';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $vector = new Builder\QueryElementVector(new Builder\RawElement('*'), 'K', 'vec', 'BLOB');
+        $vector->addParameter('EF_RUNTIME', '$EF');
+        $builder->addElement($vector);
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->addParam('K', 10)
+            ->addParam('EF', 150)
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample32(): void
+    {
+        // $expected = 'idx "*=>[KNN $K @vec $BLOB EF_RUNTIME $EF]" PARAMS 6 BLOB "\12\a9\f5\6c" K 10 EF 150';
+        // Data escaping is partially handled by the Redis lib, + this lib put extra safety ()
+        $expected = 'idx (*)=>[KNN $K @vec $BLOB EF_RUNTIME $EF] PARAMS 6 BLOB \12\a9\f5\6c K 10 EF 150';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $vector = new Builder\QueryElementVector(new Builder\RawElement('*'), '$K', '@vec', '$BLOB');
+        $vector->addParameter('EF_RUNTIME', '$EF');
+        $builder->addElement($vector);
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->addParam('K', 10)
+            ->addParam('EF', 150)
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample33Error(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+
+        $vector = new Builder\QueryElementVector(new Builder\RawElement('*'), '$K', '@vec', '$BLOB');
+        $vector->addParameter('EF_RUNTIME', 'EF');
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample4(): void
+    {
+        // $expected = 'idx "*=>[KNN $K @vec $BLOB AS my_scores]" PARAMS 4 BLOB "\12\a9\f5\6c" K 10 SORTBY my_scores';
+        // Data escaping is partially handled by the Redis lib, + this lib put extra safety ()
+        $expected = 'idx (*)=>[KNN $K @vec $BLOB AS my_scores] PARAMS 4 BLOB \12\a9\f5\6c K 10 SORTBY my_scores';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector(new Builder\RawElement('*'), 'K', 'vec', 'BLOB', 'my_scores')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->addParam('K', 10)
+            ->setSortBy('my_scores')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample5(): void
+    {
+        // $expected = 'idx "(@title:Dune @num:[2020 2022])=>[KNN $K @vec $BLOB AS my_scores]" PARAMS 4 BLOB "\12\a9\f5\6c" K 10 SORTBY my_scores';
+        // Data escaping is partially handled by the Redis lib
+        $expected = 'idx (@title:Dune @num:[2020 2022])=>[KNN $K @vec $BLOB AS my_scores] PARAMS 4 BLOB \12\a9\f5\6c K 10 SORTBY my_scores';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $group = new Builder\AndGroup([
+            new Builder\TextFacet(['title'], 'Dune'),
+            new Builder\NumericFacet(['num'], 2020, 2022),
+        ]);
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector($group, 'K', 'vec', 'BLOB', 'my_scores')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->addParam('K', 10)
+            ->setSortBy('my_scores')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
+
+    /**
+     * @see https://redis.io/docs/stack/search/reference/vectors/#examples-for-querying-vector-fields
+     */
+    public function testVectorDocExample6(): void
+    {
+        // $expected = 'idx "(@type:{shirt} ~@color:{blue})=>[KNN $K @vec $BLOB AS my_scores]" PARAMS 4 BLOB "\12\a9\f5\6c" K 10 SORTBY my_scores';
+        // Data escaping is partially handled by the Redis lib
+        $expected = 'idx (@type:{shirt} ~@color:{blue})=>[KNN $K @vec $BLOB AS my_scores] PARAMS 4 BLOB \12\a9\f5\6c K 10 SORTBY my_scores';
+        $builder = new Builder();
+        $search = new Search('2.4.0');
+
+        $group = new Builder\AndGroup([
+            new Builder\TagFacet(['type'], 'shirt'),
+            new Builder\Optional(new Builder\TagFacet(['color'], 'blue')),
+        ]);
+        $builder
+            ->addElement(
+                new Builder\QueryElementVector($group, 'K', 'vec', 'BLOB', 'my_scores')
+            )
+        ;
+        $search
+            ->setIndex('idx')
+            ->setQuery($builder->render())
+            ->addParam('BLOB', '\12\a9\f5\6c')
+            ->addParam('K', 10)
+            ->setSortBy('my_scores')
+        ;
+
+        static::assertSame($expected, implode(' ', $search->getArguments()));
+    }
 }
diff --git a/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php b/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php
new file mode 100644
index 0000000..7923ba2
--- /dev/null
+++ b/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php
@@ -0,0 +1,176 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\CreateCommand;
+
+use InvalidArgumentException;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption;
+use OutOfRangeException;
+use PHPUnit\Framework\TestCase;
+use RuntimeException;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\OptionListOption
+ *
+ * @internal
+ */
+class VectorFieldOptionTest extends TestCase
+{
+    public function testGetFieldName(): void
+    {
+        $option = new VectorFieldOption();
+        $option->setField('foo');
+
+        static::assertSame('foo', $option->getFieldName());
+    }
+
+    public function testAddFlatAttributeWrongAlgorithm(): void
+    {
+        $this->expectException(RuntimeException::class);
+        $option = new VectorFieldOption();
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_HNSW);
+        $option->addFlatAttribute(VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_IP);
+    }
+
+    public function testAddFlatAttribute(): void
+    {
+        $option = new VectorFieldOption();
+        $option->setField('vec');
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_FLAT);
+        $option->addFlatAttribute(VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_IP);
+        static::assertSame(['vec', 'VECTOR', 'FLAT',  6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP'], $option->render('2.4.0'));
+
+        $option = new VectorFieldOption();
+        $option->setField('vec');
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_FLAT);
+        $option->addFlatAttribute(VectorFieldOption::TYPE_FLOAT32, 128, VectorFieldOption::DISTANCE_METRIC_COSINE, 1000);
+        static::assertSame(['vec', 'VECTOR', 'FLAT', 8, 'TYPE', 'FLOAT32', 'DIM', 128, 'DISTANCE_METRIC', 'COSINE', 'INITIAL_CAP', 1000], $option->render('2.4.0'));
+
+        $option = new VectorFieldOption();
+        $option->setField('vec');
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_FLAT);
+        $option->addFlatAttribute(VectorFieldOption::TYPE_FLOAT32, 128, VectorFieldOption::DISTANCE_METRIC_COSINE, 1000, 10);
+        static::assertSame(['vec', 'VECTOR', 'FLAT', 10, 'TYPE', 'FLOAT32', 'DIM', 128, 'DISTANCE_METRIC', 'COSINE', 'INITIAL_CAP', 1000, 'BLOCK_SIZE', 10], $option->render('2.4.0'));
+    }
+
+    public function testAddHnswAttributeWrongAlgorithm(): void
+    {
+        $this->expectException(RuntimeException::class);
+        $option = new VectorFieldOption();
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_FLAT);
+        $option->addHnswAttribute(VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_L2);
+    }
+
+    public function testAddHnswAttribute(): void
+    {
+        $option = new VectorFieldOption();
+        $option->setField('vec');
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_HNSW);
+        $option->addHnswAttribute(VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_IP);
+        static::assertSame(['vec', 'VECTOR', 'HNSW',  6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP'], $option->render('2.4.0'));
+
+        $option = new VectorFieldOption();
+        $option->setField('vec');
+        $option->setAlgorithm(VectorFieldOption::ALGORITHM_HNSW);
+        $option->addHnswAttribute(
+            VectorFieldOption::TYPE_FLOAT32,
+            10,
+            VectorFieldOption::DISTANCE_METRIC_IP,
+            1000,
+            10,
+            500,
+            50
+        );
+        static::assertSame(['vec', 'VECTOR', 'HNSW',  14, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP', 'INITIAL_CAP', 1000, 'M', 10, 'EF_CONSTRUCTION', 500, 'EF_RUNTIME', 50], $option->render('2.4.0'));
+    }
+
+    /**
+     * @phpstan-param class-string<\Throwable> $expectedErrorClass
+     *
+     * @dataProvider validateArgumentDataProvider
+     */
+    public function testValidateArguments(string $expectedErrorClass, string $algo, string $type, int $dimension, string $distanceMetric, ?int $initialCapacity, ?int $blockSize, ?int $maxEdge, ?int $efConstruction, ?int $efRuntime): void
+    {
+        $this->expectException($expectedErrorClass);
+        $option = new VectorFieldOption();
+        $option->setField('vec');
+        $option->setAlgorithm($algo);
+        $option->addAttribute($type, $dimension, $distanceMetric, $initialCapacity, $blockSize, $maxEdge, $efConstruction, $efRuntime);
+    }
+
+    /**
+     * @return array<array<int|string>>
+     */
+    public function validateArgumentDataProvider(): array
+    {
+        $rangeDefault = [
+            0 => OutOfRangeException::class, // $expectedErrorClass
+            1 => VectorFieldOption::ALGORITHM_HNSW, // $algo
+            2 => VectorFieldOption::TYPE_FLOAT32, // $type
+            3 => 10, // $dimension
+            4 => VectorFieldOption::DISTANCE_METRIC_L2, // $distanceMetric
+            5 => 100, // $initialCapacity
+            6 => 10, // $blockSize
+            7 => 20, // $maxEdge
+            8 => 200, // $efConstruction
+            9 => 100, // $efRuntime
+        ];
+        $invalidDefault = [
+            0 => InvalidArgumentException::class, // $expectedErrorClass
+            1 => VectorFieldOption::ALGORITHM_HNSW, // $algo
+            2 => VectorFieldOption::TYPE_FLOAT32, // $type
+            3 => 10, // $dimension
+            4 => VectorFieldOption::DISTANCE_METRIC_L2, // $distanceMetric
+            5 => 100, // $initialCapacity
+            6 => 10, // $blockSize
+            7 => 20, // $maxEdge
+            8 => 200, // $efConstruction
+            9 => 100, // $efRuntime
+        ];
+
+        $sort = static function (array $source): array {
+            ksort($source);
+
+            return $source;
+        };
+
+        return [
+            $sort([2 => 'foo'] + $invalidDefault),
+            $sort([3 => 0] + $rangeDefault),
+            $sort([4 => 'bar'] + $invalidDefault),
+            $sort([5 => 0] + $rangeDefault),
+            $sort([6 => 0, 1 => VectorFieldOption::ALGORITHM_FLAT] + $rangeDefault),
+            $sort([7 => -1] + $rangeDefault),
+            $sort([8 => 0] + $rangeDefault),
+            $sort([9 => 0] + $rangeDefault),
+        ];
+    }
+}
diff --git a/tests/Redis/Command/CreateTest.php b/tests/Redis/Command/CreateTest.php
index 923ab4b..ec651b0 100644
--- a/tests/Redis/Command/CreateTest.php
+++ b/tests/Redis/Command/CreateTest.php
@@ -27,6 +27,7 @@
 use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -37,6 +38,7 @@
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\VectorFieldOption
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
@@ -48,6 +50,7 @@
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\OptionListOption
  *
  * @internal
  */
@@ -106,6 +109,13 @@ public function testFullOption(): void
             ->addTagField('tag4', null, false, true)
             ->addTagField('tag5', null, false, false, true)
             ->addTagField('tag6', '#', true, true, true)
+            ->addVectorField('vec', VectorFieldOption::ALGORITHM_FLAT, VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_L2)
+            ->addField(
+                (new VectorFieldOption())
+                    ->setField('vector_field')
+                    ->setAlgorithm(VectorFieldOption::ALGORITHM_HNSW)
+                    ->addHnswAttribute(VectorFieldOption::TYPE_FLOAT32, 128, VectorFieldOption::DISTANCE_METRIC_L2, 1000000, 40, 250, 20)
+            )
             ->setPrefixes('doc_', 'document_')
             ->setIndex('idx')
             ->setDefaultLanguage('french')
@@ -157,6 +167,8 @@ public function testFullOption(): void
             'tag4', 'TAG', 'NOINDEX',
             'tag5', 'TAG', 'CASESENSITIVE',
             'tag6', 'TAG', 'SEPARATOR', '#', 'CASESENSITIVE', 'SORTABLE', 'NOINDEX',
+            'vec', 'VECTOR', 'FLAT', 6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'L2',
+            'vector_field', 'VECTOR', 'HNSW', 14, 'TYPE', 'FLOAT32', 'DIM', 128, 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', 1000000, 'M', 40, 'EF_CONSTRUCTION', 250, 'EF_RUNTIME', 20,
         ], $command->getArguments());
     }
 
diff --git a/tests/Redis/Command/IndexListTest.php b/tests/Redis/Command/IndexListTest.php
index 0504d1d..74c82da 100644
--- a/tests/Redis/Command/IndexListTest.php
+++ b/tests/Redis/Command/IndexListTest.php
@@ -37,7 +37,7 @@ public function testGetId(): void
         static::assertSame('FT._LIST', $command->getId());
     }
 
-    public function testGetRediSearchVersion(): void
+    public function testGetSetRediSearchVersion(): void
     {
         $command = new IndexList();
 
diff --git a/tests/Redis/Command/Option/OptionListOptionTest.php b/tests/Redis/Command/Option/OptionListOptionTest.php
new file mode 100644
index 0000000..3664562
--- /dev/null
+++ b/tests/Redis/Command/Option/OptionListOptionTest.php
@@ -0,0 +1,126 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use Generator;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\Redis\Command\Option\OptionListOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\OptionListOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ *
+ * @internal
+ */
+class OptionListOptionTest extends TestCase
+{
+    use BaseOptionTestTrait;
+
+    public function testAddOption(): void
+    {
+        $emptyArgumentsOption = new OptionListOption();
+        $emptyArgumentsOption->addOption(new NamelessOption());
+        static::assertEquals([new NamelessOption()], $emptyArgumentsOption->getOptionData());
+
+        $withOneArgumentOption = new OptionListOption();
+        $withOneArgumentOption->addOption(new NamelessOption('foo'));
+        static::assertEquals([new NamelessOption('foo')], $withOneArgumentOption->getOptionData());
+
+        $withMultipleArgumentOption = new OptionListOption();
+        $withMultipleArgumentOption->addOption(new NamelessOption('foo'));
+        static::assertEquals([new NamelessOption('foo')], $withMultipleArgumentOption->getOptionData());
+        $withMultipleArgumentOption->addOption(new NamelessOption('bar'));
+        static::assertEquals([new NamelessOption('foo'), new NamelessOption('bar')], $withMultipleArgumentOption->getOptionData());
+        $withMultipleArgumentOption->addOption(new NamedOption('foobar', 'baz'));
+        static::assertEquals([new NamelessOption('foo'), new NamelessOption('bar'), new NamedOption('foobar', 'baz')], $withMultipleArgumentOption->getOptionData());
+    }
+
+    public function testSetValue(): void
+    {
+        $emptyArgumentsOption = new OptionListOption();
+        $emptyArgumentsOption->setValue(new NamelessOption());
+        static::assertEquals([new NamelessOption()], $emptyArgumentsOption->getOptionData());
+
+        $withOneArgumentOption = new OptionListOption();
+        $withOneArgumentOption->setValue(new NamelessOption('foo'));
+        static::assertEquals([new NamelessOption('foo')], $withOneArgumentOption->getOptionData());
+        $withOneArgumentOption->setValue(new NamelessOption('bar'));
+        static::assertEquals([new NamelessOption('bar')], $withOneArgumentOption->getOptionData());
+
+        $withMultipleArgumentOption = new OptionListOption();
+        $withMultipleArgumentOption->setValue(new NamelessOption('foo'), new NamelessOption('bar'), new NamedOption('foobar', 'baz'));
+        static::assertEquals([new NamelessOption('foo'), new NamelessOption('bar'), new NamedOption('foobar', 'baz')], $withMultipleArgumentOption->getOptionData());
+    }
+
+    public function dataProvider(string $testName): Generator
+    {
+        $noArgumentsOption = new OptionListOption();
+        $emptyArgumentsOption = new OptionListOption();
+        $emptyArgumentsOption->addOption(new NamelessOption());
+        $withOneArgumentOption = new OptionListOption();
+        $withOneArgumentOption->addOption(new NamelessOption('foo'));
+        $withMultipleArgumentOption = new OptionListOption();
+        $withMultipleArgumentOption->addOption(new NamelessOption('foo'));
+        $withMultipleArgumentOption->addOption(new NamelessOption('bar'));
+        $withMultipleArgumentOption->addOption(new NamedOption('foobar', 'baz'));
+
+        switch ($testName) {
+            case 'testGetOptionData':
+                yield [$noArgumentsOption, []];
+
+                yield [$emptyArgumentsOption, [new NamelessOption()]];
+
+                yield [$withOneArgumentOption, [new NamelessOption('foo')]];
+
+                yield [$withMultipleArgumentOption, [new NamelessOption('foo'), new NamelessOption('bar'), new NamedOption('foobar', 'baz')]];
+
+                break;
+
+            case 'testIsValid':
+                yield [$noArgumentsOption, true];
+
+                yield [$emptyArgumentsOption, true];
+
+                yield [$withOneArgumentOption, true];
+
+                yield [$withMultipleArgumentOption, true];
+
+                break;
+
+            case 'testRender':
+                yield [$noArgumentsOption, []];
+
+                yield [$emptyArgumentsOption, []];
+
+                yield [$withOneArgumentOption, ['foo']];
+
+                yield [$withMultipleArgumentOption, ['foo', 'bar', 'foobar', 'baz']];
+
+                break;
+        }
+    }
+}
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index ad15e84..b84298a 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\AbstractCommand;
 use MacFJA\RediSearch\Redis\Command\Search;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
@@ -29,13 +30,15 @@
 use PHPUnit\Framework\TestCase;
 
 /**
- * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
  *
+ * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\Search
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ *
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
  *
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
@@ -241,4 +244,26 @@ public function testParsingResponseScoreAndPayload(): void
 
         static::assertEquals($expectedResponse, $actualResponse);
     }
+
+    public function testParsingInvalidServerResponse(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
+        $command->parseResponse('foobar');
+    }
+
+    public function testParsingInvalidServerResponseContent(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+
+        $redisResponse = [
+            1,
+            'hash_1',
+        ];
+
+        $command = new Search(AbstractCommand::MAX_IMPLEMENTED_VERSION);
+        $command->setQuery('*');
+
+        $command->parseResponse($redisResponse);
+    }
 }

From bf556aa8eb8b367e4df89f870a5676e73ae32be0 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sat, 9 Jul 2022 22:50:03 +0200
Subject: [PATCH 50/60] RediSearch 2.4.3 compatibility

---
 CHANGELOG.md                                          |  2 ++
 src/Query/Builder/QueryElementVector.php              |  3 ++-
 src/Redis/Command/AbstractCommand.php                 |  2 +-
 src/Redis/Command/Aggregate.php                       |  8 ++++++++
 src/Redis/Command/ConfigSet.php                       |  3 ++-
 src/Redis/Command/CreateCommand/VectorFieldOption.php |  3 ++-
 src/Redis/Command/Explain.php                         | 10 ++++++++++
 src/Redis/Command/ProfileCommand/QueryOption.php      |  1 +
 src/Redis/Command/Search.php                          |  8 ++++++++
 src/Redis/Command/SpellCheck.php                      |  9 +++++++++
 tests/Redis/Command/AggregateTest.php                 |  4 +++-
 tests/Redis/Command/ExplainCliTest.php                |  7 ++++++-
 tests/Redis/Command/ExplainTest.php                   |  7 ++++++-
 tests/Redis/Command/ProfileTest.php                   |  2 +-
 tests/Redis/Command/SearchTest.php                    |  2 ++
 tests/Redis/Command/SpellCheckTest.php                |  5 ++++-
 16 files changed, 67 insertions(+), 9 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a3e8c4..023eb93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `LOAD ALL` option on Aggregate command (RediSearch `2.0.13`)
 - Profile command (RediSearch `2.2.0`) ([Issue#4])
 - Vector feature (RediSearch `2.4.0` / `2.4.2`)
+- Add `DIALECT` option (RediSearch `2.4.3`) ([Issue#35])
 - (dev) Add `make clean` to remove all generated files
 - (dev) Add code coverage to `\MacFJA\RediSearch\Index`
 - (dev) Add integration test for document insertion
@@ -234,6 +235,7 @@ First version
 [Issue#12]: https://github.com/MacFJA/php-redisearch/issues/12
 [Issue#16]: https://github.com/MacFJA/php-redisearch/issues/16
 [Issue#26]: https://github.com/MacFJA/php-redisearch/issues/26
+[Issue#35]: https://github.com/MacFJA/php-redisearch/issues/35
 [Issue#38]: https://github.com/MacFJA/php-redisearch/issues/38
 [Issue#39]: https://github.com/MacFJA/php-redisearch/issues/39
 [Issue#46]: https://github.com/MacFJA/php-redisearch/issues/46
diff --git a/src/Query/Builder/QueryElementVector.php b/src/Query/Builder/QueryElementVector.php
index c24c055..6b55c2d 100644
--- a/src/Query/Builder/QueryElementVector.php
+++ b/src/Query/Builder/QueryElementVector.php
@@ -24,11 +24,12 @@
 use function array_key_exists;
 use function count;
 use function gettype;
-use InvalidArgumentException;
 use function is_int;
 use function is_string;
 use function strlen;
 
+use InvalidArgumentException;
+
 class QueryElementVector implements QueryElementDecorator
 {
     private const PARAMETERS = [
diff --git a/src/Redis/Command/AbstractCommand.php b/src/Redis/Command/AbstractCommand.php
index 695d7f7..9acc2c4 100644
--- a/src/Redis/Command/AbstractCommand.php
+++ b/src/Redis/Command/AbstractCommand.php
@@ -33,7 +33,7 @@
  */
 abstract class AbstractCommand implements Command
 {
-    public const MAX_IMPLEMENTED_VERSION = '2.4.0';
+    public const MAX_IMPLEMENTED_VERSION = '2.4.3';
     public const MIN_IMPLEMENTED_VERSION = '2.0.0';
 
     /**
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 0cccec8..6b0512b 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -66,6 +66,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
             'limit' => new LimitOption(),
             'filter' => [],
             'cursor' => new WithCursor(),
+            'dialect' => CV::isNumeric(new NamedOption('DIALECT', null, '>=2.4.3')),
         ], $rediSearchVersion);
     }
 
@@ -168,6 +169,13 @@ public function setLoadAll(): self
         return $this;
     }
 
+    public function setDialect(int $version): self
+    {
+        $this->options['dialect']->setValue($version);
+
+        return $this;
+    }
+
     public function setWithCursor(?int $count = null, ?int $maxIdle = null): self
     {
         $this->options['cursor']
diff --git a/src/Redis/Command/ConfigSet.php b/src/Redis/Command/ConfigSet.php
index 3702f0b..baefb46 100644
--- a/src/Redis/Command/ConfigSet.php
+++ b/src/Redis/Command/ConfigSet.php
@@ -33,7 +33,8 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
             'action' => new FlagOption('SET', true, '>=2.0.0'),
             'options' => [
                 CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.0 <2.0.6'), ['NOGC', 'MAXEXPANSIONS', 'TIMEOUT', 'ON_TIMEOUT', 'MIN_PHONETIC_TERM_LEN']),
-                CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.6'), ['NOGC', 'MINPREFIX', 'MAXEXPANSIONS', 'MAXFILTEREXPANSION', 'TIMEOUT', 'ON_TIMEOUT', 'MIN_PHONETIC_TERM_LEN']),
+                CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.0.6 <2.4.3'), ['NOGC', 'MINPREFIX', 'MAXEXPANSIONS', 'MAXFILTEREXPANSION', 'TIMEOUT', 'ON_TIMEOUT', 'MIN_PHONETIC_TERM_LEN']),
+                CustomValidatorOption::allowedValues(new NamelessOption(null, '>=2.4.3'), ['NOGC', 'MINPREFIX', 'MAXEXPANSIONS', 'TIMEOUT', 'ON_TIMEOUT', 'MIN_PHONETIC_TERM_LEN', 'DEFAULT_DIALECT']),
             ],
             'value' => new NamelessOption(null, '>=2.0.0'),
         ], $rediSearchVersion);
diff --git a/src/Redis/Command/CreateCommand/VectorFieldOption.php b/src/Redis/Command/CreateCommand/VectorFieldOption.php
index 98ea81b..ecdd146 100644
--- a/src/Redis/Command/CreateCommand/VectorFieldOption.php
+++ b/src/Redis/Command/CreateCommand/VectorFieldOption.php
@@ -22,8 +22,9 @@
 namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
 
 use function in_array;
-use InvalidArgumentException;
 use function is_int;
+
+use InvalidArgumentException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
 use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
diff --git a/src/Redis/Command/Explain.php b/src/Redis/Command/Explain.php
index a4c8358..c5059df 100644
--- a/src/Redis/Command/Explain.php
+++ b/src/Redis/Command/Explain.php
@@ -21,6 +21,8 @@
 
 namespace MacFJA\RediSearch\Redis\Command;
 
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
+use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 
 /**
@@ -33,6 +35,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
         parent::__construct([
             'index' => new NamelessOption(null, '>=2.0.0'),
             'query' => new NamelessOption(null, '>=2.0.0'),
+            'dialect' => CV::isNumeric(new NamedOption('DIALECT', null, '>=2.4.3')),
         ], $rediSearchVersion);
     }
 
@@ -50,6 +53,13 @@ public function setQuery(string $query): self
         return $this;
     }
 
+    public function setDialect(int $version): self
+    {
+        $this->options['dialect']->setValue($version);
+
+        return $this;
+    }
+
     public function getId(): string
     {
         return 'FT.EXPLAIN';
diff --git a/src/Redis/Command/ProfileCommand/QueryOption.php b/src/Redis/Command/ProfileCommand/QueryOption.php
index 3865ddb..b7d1723 100644
--- a/src/Redis/Command/ProfileCommand/QueryOption.php
+++ b/src/Redis/Command/ProfileCommand/QueryOption.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\Redis\Command\ProfileCommand;
 
 use function assert;
+
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 use MacFJA\RediSearch\Redis\Command\Search;
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 3b6f6fa..6dbd429 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -79,6 +79,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
                 'params' => new NotEmptyOption(new NumberedOption('PARAMS', null, '>=2.4.0')),
                 'sortby' => new SortByOption(),
                 'limit' => new LimitOption(),
+                'dialect' => CV::isNumeric(new NamedOption('DIALECT', null, '>=2.4.3')),
             ],
             $rediSearchVersion
         );
@@ -323,6 +324,13 @@ public function getSize(): ?int
         return $limit->getSize();
     }
 
+    public function setDialect(int $version): self
+    {
+        $this->options['dialect']->setValue($version);
+
+        return $this;
+    }
+
     /**
      * @param mixed $data
      *
diff --git a/src/Redis/Command/SpellCheck.php b/src/Redis/Command/SpellCheck.php
index 45e2e97..c1301d5 100644
--- a/src/Redis/Command/SpellCheck.php
+++ b/src/Redis/Command/SpellCheck.php
@@ -25,6 +25,7 @@
 
 use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
+use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption as CV;
 use MacFJA\RediSearch\Redis\Command\Option\NamedOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption;
@@ -42,6 +43,7 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE
                 new NamedOption('DISTANCE', null, '>=2.0.0'),
                 Validator::intVal()->between(1, 4)
             ),
+            'dialect' => CV::isNumeric(new NamedOption('DIALECT', null, '>=2.4.3')),
             'terms' => [],
         ], $rediSearchVersion);
     }
@@ -67,6 +69,13 @@ public function setDistance(int $distance): self
         return $this;
     }
 
+    public function setDialect(int $version): self
+    {
+        $this->options['dialect']->setValue($version);
+
+        return $this;
+    }
+
     public function addTerms(string $dictionary, bool $isExcluding = false): self
     {
         $terms = new TermsOption();
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index 30f623b..43c0c51 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -165,7 +165,7 @@ public function testLoadAll(): void
 
     public function testFullOption(): void
     {
-        $command = new Aggregate();
+        $command = new Aggregate(Aggregate::MAX_IMPLEMENTED_VERSION);
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')
@@ -179,6 +179,7 @@ public function testFullOption(): void
             ->setLimit(12, 35)
             ->addApply('@timestamp - (@timestamp % 86400)', 'day')
             ->setWithCursor(20, 30)
+            ->setDialect(2)
         ;
 
         static::assertSame([
@@ -192,6 +193,7 @@ public function testFullOption(): void
             'LIMIT', 12, 35,
             'FILTER', "@name=='foo' && @age < 20",
             'WITHCURSOR', 'COUNT', 20, 'MAXIDLE', 30,
+            'DIALECT', 2,
         ], $command->getArguments());
     }
 
diff --git a/tests/Redis/Command/ExplainCliTest.php b/tests/Redis/Command/ExplainCliTest.php
index 3f38723..9e049cd 100644
--- a/tests/Redis/Command/ExplainCliTest.php
+++ b/tests/Redis/Command/ExplainCliTest.php
@@ -31,6 +31,9 @@
  * @covers \MacFJA\RediSearch\Redis\Command\ExplainCli
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  *
  * @internal
@@ -45,15 +48,17 @@ public function testGetId(): void
 
     public function testFullOption(): void
     {
-        $command = new ExplainCli();
+        $command = new ExplainCli(ExplainCli::MAX_IMPLEMENTED_VERSION);
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')
+            ->setDialect(2)
         ;
 
         static::assertSame([
             'idx',
             '@text1:"hello world"',
+            'DIALECT', 2,
         ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/ExplainTest.php b/tests/Redis/Command/ExplainTest.php
index 9497ef8..052575e 100644
--- a/tests/Redis/Command/ExplainTest.php
+++ b/tests/Redis/Command/ExplainTest.php
@@ -30,6 +30,9 @@
  * @covers \MacFJA\RediSearch\Redis\Command\Explain
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  *
  * @internal
@@ -44,15 +47,17 @@ public function testGetId(): void
 
     public function testFullOption(): void
     {
-        $command = new Explain();
+        $command = new Explain(Explain::MAX_IMPLEMENTED_VERSION);
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')
+            ->setDialect(2)
         ;
 
         static::assertSame([
             'idx',
             '@text1:"hello world"',
+            'DIALECT', 2,
         ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/ProfileTest.php b/tests/Redis/Command/ProfileTest.php
index a7b985e..a3ac5b1 100644
--- a/tests/Redis/Command/ProfileTest.php
+++ b/tests/Redis/Command/ProfileTest.php
@@ -70,7 +70,7 @@ public function testFullOption(): void
             ->setTypeAggregate()
             ->setLimited()
             ->setQuery($aggregate)
-            ;
+        ;
 
         static::assertSame([
             'idx', 'AGGREGATE', 'LIMITED', 'QUERY', '@foo:bar',
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index b84298a..cee08f9 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -112,6 +112,7 @@ public function testFullOption(): void
             ->setPayload('foobar')
             ->setSortBy('@num1', 'ASC')
             ->setLimit(12, 10)
+            ->setDialect(2)
         ;
 
         static::assertSame([
@@ -138,6 +139,7 @@ public function testFullOption(): void
             'PAYLOAD', 'foobar',
             'SORTBY', '@num1', 'ASC',
             'LIMIT', 12, 10,
+            'DIALECT', 2,
         ], $command->getArguments());
     }
 
diff --git a/tests/Redis/Command/SpellCheckTest.php b/tests/Redis/Command/SpellCheckTest.php
index 02a387b..6491ae2 100644
--- a/tests/Redis/Command/SpellCheckTest.php
+++ b/tests/Redis/Command/SpellCheckTest.php
@@ -36,6 +36,7 @@
  **
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
  *
@@ -51,19 +52,21 @@ public function testGetId(): void
 
     public function testFullOption(): void
     {
-        $command = new SpellCheck();
+        $command = new SpellCheck(SpellCheck::MAX_IMPLEMENTED_VERSION);
         $command
             ->setIndex('idx')
             ->setQuery('@text1:"hello world"')
             ->setDistance(2)
             ->addTerms('badword', true)
             ->addTerms('cities', false)
+            ->setDialect(2)
         ;
 
         static::assertSame([
             'idx',
             '@text1:"hello world"',
             'DISTANCE', 2,
+            'DIALECT', 2,
             'TERMS', 'EXCLUDE', 'badword',
             'TERMS', 'INCLUDE', 'cities',
         ], $command->getArguments());

From 4a1692e3db94540be2d50d4a684edb357d0387d3 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Wed, 27 Jul 2022 23:21:53 +0200
Subject: [PATCH 51/60] Set option to Phpredis to return raw response

---
 CHANGELOG.md                        |  1 +
 phpstan.neon                        |  2 ++
 src/Redis/Client/PhpredisClient.php | 10 +++++++++-
 3 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 118234c..3b7cc71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Missing default values on highlight option ([Issue#38])
 - Don't throw an exception if the result of the index definition is incorrect ([Issue#46])
+- Phpredis extension don't return raw response ([Issue#46])
 
 ### Changed
 
diff --git a/phpstan.neon b/phpstan.neon
index 78d090b..5e00e5c 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -3,6 +3,8 @@ parameters:
     paths:
         - src
         - tests
+    stubFiles:
+        - vendor/ukko/phpredis-phpdoc/src/Redis.php
     excludePaths:
         analyse:
             - tests/psalm/stubs/phpiredis.php
diff --git a/src/Redis/Client/PhpredisClient.php b/src/Redis/Client/PhpredisClient.php
index d20c5dd..d45001b 100644
--- a/src/Redis/Client/PhpredisClient.php
+++ b/src/Redis/Client/PhpredisClient.php
@@ -40,7 +40,7 @@ private function __construct(Redis $redis)
     {
         if (!static::supports($redis)) {
             throw new RuntimeException($this->getMissingMessage('phpredis', true, [
-                Redis::class => ['rawCommand', 'multi', 'exec'],
+                Redis::class => ['rawCommand', 'multi', 'exec', 'getOption', 'setOption'],
             ]));
         }
         $this->redis = $redis;
@@ -49,6 +49,10 @@ private function __construct(Redis $redis)
     public function execute(Command $command)
     {
         $arguments = $command->getArguments();
+
+        $originalValue = $this->redis->getOption(Redis::OPT_REPLY_LITERAL) ?? false;
+        $this->redis->setOption(Redis::OPT_REPLY_LITERAL, true);
+
         if (0 === count($arguments)) {
             /** @psalm-suppress TooFewArguments */
             $rawResponse = $this->redis->rawCommand($command->getId());
@@ -56,6 +60,8 @@ public function execute(Command $command)
             $rawResponse = $this->redis->rawCommand($command->getId(), ...$arguments);
         }
 
+        $this->redis->setOption(Redis::OPT_REPLY_LITERAL, $originalValue);
+
         return $command->parseResponse($rawResponse);
     }
 
@@ -64,6 +70,8 @@ public static function supports($redis): bool
         return $redis instanceof Redis
             && method_exists($redis, 'rawCommand')
             && method_exists($redis, 'multi')
+            && method_exists($redis, 'setOption')
+            && method_exists($redis, 'getOption')
             && method_exists($redis, 'exec');
     }
 

From fe519c52b4277377bd1345e3622ee82cab6ed416 Mon Sep 17 00:00:00 2001
From: Vincent Radstake <vlradstake@gmail.com>
Date: Tue, 14 Jun 2022 16:44:22 +0200
Subject: [PATCH 52/60] Return empty modules array when module command is not
 available

---
 src/Redis/Initializer.php | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 865f3af..7c5d0a9 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -139,7 +139,14 @@ public static function registerCommandsRediska(): void
 
     public static function getRediSearchVersion(Client $client): ?string
     {
-        $modules = $client->executeRaw('module', 'list') ?? [];
+        try {
+            $modules = $client->executeRaw('module', 'list') ?? [];
+        } catch (\Throwable $exception) {
+            if (strpos($exception->getMessage(), 'unknown command') === false) {
+                throw $exception;
+            }
+            $modules = [];
+        }
 
         foreach ($modules as $module) {
             $data = array_column(

From dbf3e4a5baa833ab226b3ec625b87f854e207283 Mon Sep 17 00:00:00 2001
From: Vincent Radstake <vlradstake@gmail.com>
Date: Tue, 14 Jun 2022 17:00:17 +0200
Subject: [PATCH 53/60] Update Initializer.php

---
 src/Redis/Initializer.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 7c5d0a9..a15de99 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -142,7 +142,7 @@ public static function getRediSearchVersion(Client $client): ?string
         try {
             $modules = $client->executeRaw('module', 'list') ?? [];
         } catch (\Throwable $exception) {
-            if (strpos($exception->getMessage(), 'unknown command') === false) {
+            if (strpos($exception->getMessage(), 'unknown command') !== false) {
                 throw $exception;
             }
             $modules = [];

From f807d02cac347ce0a24d1cc21f8c2bef59c24c6c Mon Sep 17 00:00:00 2001
From: Vincent Radstake <vlradstake@gmail.com>
Date: Tue, 14 Jun 2022 18:00:11 +0200
Subject: [PATCH 54/60] Update Initializer.php

---
 src/Redis/Initializer.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index a15de99..7c5d0a9 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -142,7 +142,7 @@ public static function getRediSearchVersion(Client $client): ?string
         try {
             $modules = $client->executeRaw('module', 'list') ?? [];
         } catch (\Throwable $exception) {
-            if (strpos($exception->getMessage(), 'unknown command') !== false) {
+            if (strpos($exception->getMessage(), 'unknown command') === false) {
                 throw $exception;
             }
             $modules = [];

From 029853fcc4f83548eb13980e72b50563e533f672 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Fri, 5 Aug 2022 23:32:55 +0200
Subject: [PATCH 55/60] Add UT + update Changelog

---
 CHANGELOG.md              |  2 ++
 src/Redis/Initializer.php |  8 +++---
 tests/fixes/PR42Test.php  | 55 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 62 insertions(+), 3 deletions(-)
 create mode 100644 tests/fixes/PR42Test.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e36b147..e052485 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Missing default values on highlight option ([Issue#38])
 - Don't throw an exception if the result of the index definition is incorrect ([Issue#46])
 - Phpredis extension don't return raw response ([Issue#46])
+- Error with version reading if `MODULE LIST` command is protected ([PR#42])
 
 ### Changed
 
@@ -247,3 +248,4 @@ First version
 [PR#14]: https://github.com/MacFJA/php-redisearch/pull/14
 [PR#15]: https://github.com/MacFJA/php-redisearch/pull/15
 [PR#17]: https://github.com/MacFJA/php-redisearch/pull/17
+[PR#42]: https://github.com/MacFJA/php-redisearch/pull/42
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 7c5d0a9..8c72dbf 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -52,6 +52,7 @@
 use MacFJA\RediSearch\Redis\Command\TagVals;
 use Predis\Profile\RedisProfile;
 use Rediska_Commands;
+use Throwable;
 
 /**
  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -141,11 +142,12 @@ public static function getRediSearchVersion(Client $client): ?string
     {
         try {
             $modules = $client->executeRaw('module', 'list') ?? [];
-        } catch (\Throwable $exception) {
-            if (strpos($exception->getMessage(), 'unknown command') === false) {
+        } catch (Throwable $exception) {
+            if (false === stripos($exception->getMessage(), 'unknown command')) {
                 throw $exception;
             }
-            $modules = [];
+
+            return null;
         }
 
         foreach ($modules as $module) {
diff --git a/tests/fixes/PR42Test.php b/tests/fixes/PR42Test.php
new file mode 100644
index 0000000..07063cf
--- /dev/null
+++ b/tests/fixes/PR42Test.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixes;
+
+use Exception;
+use MacFJA\RediSearch\Redis\Client;
+use MacFJA\RediSearch\Redis\Initializer;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Initializer
+ *
+ * @internal
+ */
+class PR42Test extends TestCase
+{
+    public function testUnavailableCommand(): void
+    {
+        $client = $this->createMock(Client::class);
+        $client->method('executeRaw')->willThrowException(new Exception('ERR unknown command `module`, with args beginning with: `list`'));
+
+        $response = Initializer::getRediSearchVersion($client);
+
+        static::assertNull($response);
+    }
+
+    public function testUnknownError(): void
+    {
+        $this->expectException(Exception::class);
+
+        $client = $this->createMock(Client::class);
+        $client->method('executeRaw')->willThrowException(new Exception('ERR unknown server error'));
+
+        Initializer::getRediSearchVersion($client);
+    }
+}

From ec9dec1dd34c07011224eec7ae3057e87ceb2bd4 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Sun, 14 Aug 2022 00:10:58 +0200
Subject: [PATCH 56/60] Version 2.2.0

---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e052485..602394f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+## [2.2.0]
+
 ### Added
 
 - Documentation on paginated responses
@@ -213,7 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 First version
 
-[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.1.2...HEAD
+[Unreleased v2.x]: https://github.com/MacFJA/php-redisearch/compare/2.2.0...HEAD
+[2.2.0]: https://github.com/MacFJA/php-redisearch/compare/2.1.2...2.2.0
 [2.1.2]: https://github.com/MacFJA/php-redisearch/compare/2.1.1...2.1.2
 [2.1.1]: https://github.com/MacFJA/php-redisearch/compare/2.1.0...2.1.1
 [2.1.0]: https://github.com/MacFJA/php-redisearch/compare/2.0.2...2.1.0

From a9253564a20f0ad0cd0205fa3b5d50e75a17261d Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Wed, 19 Oct 2022 19:54:20 +0200
Subject: [PATCH 57/60] Update PHP-CS-Fixer version

---
 CHANGELOG.md                                              | 4 ++++
 src/Redis/Client/PhpiredisClient.php                      | 1 +
 src/Redis/Client/PredisClient.php                         | 1 +
 src/Redis/Command/Aggregate.php                           | 1 +
 src/Redis/Command/Option/CustomValidatorOption.php        | 3 +++
 src/Redis/Command/Option/DecoratedOptionTrait.php         | 2 ++
 src/Redis/Command/Option/FlagOption.php                   | 1 +
 src/Redis/Command/Option/NotEmptyOption.php               | 1 +
 src/Redis/Command/Search.php                              | 1 +
 src/Redis/Initializer.php                                 | 1 +
 src/Redis/Response/AggregateResponseItem.php              | 3 +++
 src/Redis/Response/InfoResponse.php                       | 3 +--
 tests/IndexBuilderTest.php                                | 1 +
 tests/IndexTest.php                                       | 3 ++-
 tests/Query/Builder/FuzzyTest.php                         | 3 ++-
 tests/Query/Builder/GeoFacetTest.php                      | 3 ++-
 tests/Query/Builder/IncludeSpaceTest.php                  | 6 ++----
 tests/Query/Builder/IncompleteFacetTest.php               | 1 -
 tests/Query/Builder/PriorityTest.php                      | 6 ++----
 tests/Query/Builder/QueryElementAttributeTest.php         | 3 ++-
 tests/Query/BuilderTest.php                               | 1 -
 tests/Query/EscaperTest.php                               | 3 ++-
 tests/Redis/Command/AbstractCommandTest.php               | 3 ++-
 tests/Redis/Command/AddFieldOptionTraitTest.php           | 3 ++-
 tests/Redis/Command/AggregateTest.php                     | 8 +++-----
 .../Redis/Command/AggregationCommand/ReduceOptionTest.php | 3 ++-
 tests/Redis/Command/AliasAddTest.php                      | 1 -
 tests/Redis/Command/AliasDelTest.php                      | 1 -
 tests/Redis/Command/AliasUpdateTest.php                   | 1 -
 tests/Redis/Command/AlterTest.php                         | 1 -
 .../Redis/Command/CreateCommand/VectorFieldOptionTest.php | 6 +++---
 tests/Redis/Command/CreateTest.php                        | 2 +-
 tests/Redis/Command/DictAddTest.php                       | 1 -
 tests/Redis/Command/DictDelTest.php                       | 1 -
 tests/Redis/Command/DictDumpTest.php                      | 1 -
 tests/Redis/Command/DropIndexTest.php                     | 1 -
 tests/Redis/Command/ExplainCliTest.php                    | 1 -
 tests/Redis/Command/ExplainTest.php                       | 1 -
 tests/Redis/Command/InfoTest.php                          | 2 --
 tests/Redis/Command/Option/AbstractCommandOptionTest.php  | 3 ++-
 tests/Redis/Command/Option/CustomValidatorOptionTest.php  | 3 ++-
 tests/Redis/Command/ProfileTest.php                       | 1 -
 tests/Redis/Command/SearchCommand/FilterOptionTest.php    | 3 ++-
 tests/Redis/Command/SearchCommand/LimitOptionTest.php     | 3 ++-
 tests/Redis/Command/SearchTest.php                        | 4 ----
 tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php | 3 ++-
 tests/Redis/Command/SpellCheckTest.php                    | 2 --
 tests/Redis/Command/SugAddTest.php                        | 1 -
 tests/Redis/Command/SugDelTest.php                        | 1 -
 tests/Redis/Command/SugGetTest.php                        | 3 +--
 tests/Redis/Command/SugLenTest.php                        | 1 -
 tests/Redis/Command/SynDumpTest.php                       | 1 -
 tests/Redis/Command/SynUpdateTest.php                     | 3 +--
 tests/Redis/Command/TagValsTest.php                       | 1 -
 tests/Redis/InitializerTest.php                           | 4 +++-
 tests/integration/DockerTest.php                          | 2 ++
 56 files changed, 65 insertions(+), 63 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 602394f..15e571f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Changed
+
+- (dev) Update PHP-CS-Fixer to version `3.12.0`
+
 ## [2.2.0]
 
 ### Added
diff --git a/src/Redis/Client/PhpiredisClient.php b/src/Redis/Client/PhpiredisClient.php
index d371fb7..0c30a6b 100644
--- a/src/Redis/Client/PhpiredisClient.php
+++ b/src/Redis/Client/PhpiredisClient.php
@@ -39,6 +39,7 @@ class PhpiredisClient extends AbstractClient
 
     /**
      * @param resource $redis
+     *
      * @psalm-assert function_exists('phpiredis_command_bs')
      * @psalm-assert function_exists('phpiredis_multi_command_bs')
      */
diff --git a/src/Redis/Client/PredisClient.php b/src/Redis/Client/PredisClient.php
index adc1fe0..89cf769 100644
--- a/src/Redis/Client/PredisClient.php
+++ b/src/Redis/Client/PredisClient.php
@@ -104,6 +104,7 @@ protected function doPipeline(Command ...$commands): array
 
     /**
      * @param array<float|int|string> $args
+     *
      * @codeCoverageIgnore
      */
     private static function createRawCommand(array $args): CommandInterface
diff --git a/src/Redis/Command/Aggregate.php b/src/Redis/Command/Aggregate.php
index 6b0512b..1493473 100644
--- a/src/Redis/Command/Aggregate.php
+++ b/src/Redis/Command/Aggregate.php
@@ -219,6 +219,7 @@ public function getSize(): ?int
      * @param array|mixed|string $data
      *
      * @return CursorResponse|PaginatedResponse<AggregateResponseItem>
+     *
      * @phpstan-return CursorResponse|PaginatedResponse
      */
     public function parseResponse($data)
diff --git a/src/Redis/Command/Option/CustomValidatorOption.php b/src/Redis/Command/Option/CustomValidatorOption.php
index 02c8736..89abc90 100644
--- a/src/Redis/Command/Option/CustomValidatorOption.php
+++ b/src/Redis/Command/Option/CustomValidatorOption.php
@@ -27,6 +27,7 @@
 
 /**
  * @template T of CommandOption
+ *
  * @mixin T
  */
 class CustomValidatorOption implements CommandOption, DecoratedCommandOption
@@ -47,6 +48,7 @@ public function __construct(CommandOption $decorated, Validatable $validator)
 
     /**
      * @phpstan-param T $decorated
+     *
      * @psalm-param CommandOption $decorated
      *
      * @param array<float|int|string> $allowed
@@ -60,6 +62,7 @@ public static function allowedValues(CommandOption $decorated, array $allowed):
 
     /**
      * @phpstan-param T $decorated
+     *
      * @psalm-param CommandOption $decorated
      *
      * @return static<T>
diff --git a/src/Redis/Command/Option/DecoratedOptionTrait.php b/src/Redis/Command/Option/DecoratedOptionTrait.php
index 4640833..b84ec8a 100644
--- a/src/Redis/Command/Option/DecoratedOptionTrait.php
+++ b/src/Redis/Command/Option/DecoratedOptionTrait.php
@@ -25,12 +25,14 @@
 
 /**
  * @template T of CommandOption
+ *
  * @mixin T
  */
 trait DecoratedOptionTrait
 {
     /**
      * @var CommandOption
+     *
      * @phpstan-var T
      */
     private $decorated;
diff --git a/src/Redis/Command/Option/FlagOption.php b/src/Redis/Command/Option/FlagOption.php
index 992c216..b565bfe 100644
--- a/src/Redis/Command/Option/FlagOption.php
+++ b/src/Redis/Command/Option/FlagOption.php
@@ -59,6 +59,7 @@ public function isActive(): bool
 
     /**
      * @return bool
+     *
      * @SuppressWarnings(PHPMD.BooleanGetMethodName)
      */
     public function getOptionData()
diff --git a/src/Redis/Command/Option/NotEmptyOption.php b/src/Redis/Command/Option/NotEmptyOption.php
index 76029ad..8dcf4c5 100644
--- a/src/Redis/Command/Option/NotEmptyOption.php
+++ b/src/Redis/Command/Option/NotEmptyOption.php
@@ -25,6 +25,7 @@
 
 /**
  * @template T of CommandOption
+ *
  * @mixin T
  */
 class NotEmptyOption implements CommandOption, DecoratedCommandOption
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 6dbd429..7aa9639 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -335,6 +335,7 @@ public function setDialect(int $version): self
      * @param mixed $data
      *
      * @return PaginatedResponse
+     *
      * @psalm-return PaginatedResponse<SearchResponseItem>
      *
      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index 8c72dbf..bc39538 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -61,6 +61,7 @@ class Initializer
 {
     /**
      * @codeCoverageIgnore
+     *
      * @psalm-suppress UndefinedDocblockClass
      * @psalm-suppress UndefinedClass
      *
diff --git a/src/Redis/Response/AggregateResponseItem.php b/src/Redis/Response/AggregateResponseItem.php
index 1f6dbed..9c18f12 100644
--- a/src/Redis/Response/AggregateResponseItem.php
+++ b/src/Redis/Response/AggregateResponseItem.php
@@ -51,8 +51,11 @@ public function getFields(): array
      * @return null|array<mixed>|float|int|string
      *
      * @deprecated Use ::getFieldValue instead
+     *
      * @codeCoverageIgnore
+     *
      * @psalm-suppress
+     *
      * @SuppressWarnings
      */
     public function getValue(string $fieldName, $default = null)
diff --git a/src/Redis/Response/InfoResponse.php b/src/Redis/Response/InfoResponse.php
index 897d7ef..0fd2b6e 100644
--- a/src/Redis/Response/InfoResponse.php
+++ b/src/Redis/Response/InfoResponse.php
@@ -159,9 +159,8 @@ public function getFieldsAsOption(): array
             assert($object instanceof GroupedOption);
             $object->setDataOfOption('field', $fieldName);
             $object = self::setOptionFlag($object, $field, 'NOINDEX', 'no_index');
-            $object = self::setOptionFlag($object, $field, 'SORTABLE', 'sortable');
 
-            return $object;
+            return self::setOptionFlag($object, $field, 'SORTABLE', 'sortable');
         }, $fields);
     }
 
diff --git a/tests/IndexBuilderTest.php b/tests/IndexBuilderTest.php
index bcff32a..11adf69 100644
--- a/tests/IndexBuilderTest.php
+++ b/tests/IndexBuilderTest.php
@@ -64,6 +64,7 @@ class IndexBuilderTest extends TestCase
 {
     /**
      * @return array<Create|IndexBuilder>
+     *
      * @medium
      */
     public function testAllSetter(): array
diff --git a/tests/IndexTest.php b/tests/IndexTest.php
index 51a9e1e..6e7c47a 100644
--- a/tests/IndexTest.php
+++ b/tests/IndexTest.php
@@ -32,6 +32,7 @@
 use MacFJA\RediSearch\Redis\Command\DropIndex;
 use MacFJA\RediSearch\Redis\Command\TagVals;
 use MacFJA\RediSearch\Redis\Response\InfoResponse;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Index
@@ -60,7 +61,7 @@
  *
  * @internal
  */
-class IndexTest extends \PHPUnit\Framework\TestCase
+class IndexTest extends TestCase
 {
     public function testAddDocumentFromArray(): void
     {
diff --git a/tests/Query/Builder/FuzzyTest.php b/tests/Query/Builder/FuzzyTest.php
index a8af50c..7d50899 100644
--- a/tests/Query/Builder/FuzzyTest.php
+++ b/tests/Query/Builder/FuzzyTest.php
@@ -23,6 +23,7 @@
 
 use MacFJA\RediSearch\Exception\OutOfRangeLevenshteinDistanceException;
 use MacFJA\RediSearch\Query\Builder\Fuzzy;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Exception\OutOfRangeLevenshteinDistanceException
@@ -30,7 +31,7 @@
  *
  * @internal
  */
-class FuzzyTest extends \PHPUnit\Framework\TestCase
+class FuzzyTest extends TestCase
 {
     public function testTooLow(): void
     {
diff --git a/tests/Query/Builder/GeoFacetTest.php b/tests/Query/Builder/GeoFacetTest.php
index 7d4877c..d590160 100644
--- a/tests/Query/Builder/GeoFacetTest.php
+++ b/tests/Query/Builder/GeoFacetTest.php
@@ -23,6 +23,7 @@
 
 use MacFJA\RediSearch\Exception\UnknownUnitException;
 use MacFJA\RediSearch\Query\Builder\GeoFacet;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Exception\UnknownUnitException
@@ -30,7 +31,7 @@
  *
  * @internal
  */
-class GeoFacetTest extends \PHPUnit\Framework\TestCase
+class GeoFacetTest extends TestCase
 {
     public function testWrongUnit(): void
     {
diff --git a/tests/Query/Builder/IncludeSpaceTest.php b/tests/Query/Builder/IncludeSpaceTest.php
index 5c5e252..07fafb5 100644
--- a/tests/Query/Builder/IncludeSpaceTest.php
+++ b/tests/Query/Builder/IncludeSpaceTest.php
@@ -39,22 +39,20 @@
 use MacFJA\RediSearch\Query\Builder\TextFacet;
 use MacFJA\RediSearch\Query\Builder\Word;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Query\Builder
- *
  * @covers \MacFJA\RediSearch\Query\Builder\AbstractGroup
  * @covers \MacFJA\RediSearch\Query\Builder\AndGroup
  * @covers \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
  * @covers \MacFJA\RediSearch\Query\Builder\ExactMatch
  * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
- *
  * @covers \MacFJA\RediSearch\Query\Builder\Fuzzy
  * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
  * @covers \MacFJA\RediSearch\Query\Builder\Negation
  * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
  * @covers \MacFJA\RediSearch\Query\Builder\Optional
- *
  * @covers \MacFJA\RediSearch\Query\Builder\OrGroup
  * @covers \MacFJA\RediSearch\Query\Builder\Prefix
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElement::priority
@@ -69,7 +67,7 @@
  *
  * @internal
  */
-class IncludeSpaceTest extends \PHPUnit\Framework\TestCase
+class IncludeSpaceTest extends TestCase
 {
     public function testGroupIncludeSpace(): void
     {
diff --git a/tests/Query/Builder/IncompleteFacetTest.php b/tests/Query/Builder/IncompleteFacetTest.php
index a3a530a..ad07d6b 100644
--- a/tests/Query/Builder/IncompleteFacetTest.php
+++ b/tests/Query/Builder/IncompleteFacetTest.php
@@ -35,7 +35,6 @@
 /**
  * @covers \MacFJA\RediSearch\Exception\NotEnoughFieldsException
  * @covers \MacFJA\RediSearch\Exception\NotEnoughTermsException
- *
  * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
  * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
  * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
diff --git a/tests/Query/Builder/PriorityTest.php b/tests/Query/Builder/PriorityTest.php
index 18e8bb3..43c2ae4 100644
--- a/tests/Query/Builder/PriorityTest.php
+++ b/tests/Query/Builder/PriorityTest.php
@@ -40,22 +40,20 @@
 use MacFJA\RediSearch\Query\Builder\TextFacet;
 use MacFJA\RediSearch\Query\Builder\Word;
 use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Query\Builder
- *
  * @covers \MacFJA\RediSearch\Query\Builder\AbstractGroup
  * @covers \MacFJA\RediSearch\Query\Builder\AndGroup
  * @covers \MacFJA\RediSearch\Query\Builder\EncapsulationGroup
  * @covers \MacFJA\RediSearch\Query\Builder\ExactMatch
  * @covers \MacFJA\RediSearch\Query\Builder\FieldFacet
- *
  * @covers \MacFJA\RediSearch\Query\Builder\Fuzzy
  * @covers \MacFJA\RediSearch\Query\Builder\GeoFacet
  * @covers \MacFJA\RediSearch\Query\Builder\Negation
  * @covers \MacFJA\RediSearch\Query\Builder\NumericFacet
  * @covers \MacFJA\RediSearch\Query\Builder\Optional
- *
  * @covers \MacFJA\RediSearch\Query\Builder\OrGroup
  * @covers \MacFJA\RediSearch\Query\Builder\Prefix
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElement::priority
@@ -70,7 +68,7 @@
  *
  * @internal
  */
-class PriorityTest extends \PHPUnit\Framework\TestCase
+class PriorityTest extends TestCase
 {
     public function testGroupPriority(): void
     {
diff --git a/tests/Query/Builder/QueryElementAttributeTest.php b/tests/Query/Builder/QueryElementAttributeTest.php
index d3d86b5..0baf2a5 100644
--- a/tests/Query/Builder/QueryElementAttributeTest.php
+++ b/tests/Query/Builder/QueryElementAttributeTest.php
@@ -23,6 +23,7 @@
 
 use MacFJA\RediSearch\Query\Builder\QueryElementAttribute;
 use MacFJA\RediSearch\Query\Builder\RawElement;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Query\Builder\QueryElementAttribute
@@ -32,7 +33,7 @@
  *
  * @internal
  */
-class QueryElementAttributeTest extends \PHPUnit\Framework\TestCase
+class QueryElementAttributeTest extends TestCase
 {
     /**
      * @dataProvider dataProvider
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index e1ec1d7..625f878 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -47,7 +47,6 @@
  * @covers \MacFJA\RediSearch\Query\Builder\TextFacet
  * @covers \MacFJA\RediSearch\Query\Builder\Word
  * @covers \MacFJA\RediSearch\Query\Escaper
- *
  * @covers \MacFJA\RediSearch\Redis\Command\Search::addParam
  *
  * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
diff --git a/tests/Query/EscaperTest.php b/tests/Query/EscaperTest.php
index bcbcb80..11fdc0d 100644
--- a/tests/Query/EscaperTest.php
+++ b/tests/Query/EscaperTest.php
@@ -24,13 +24,14 @@
 use function count;
 
 use MacFJA\RediSearch\Query\Escaper;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Query\Escaper
  *
  * @internal
  */
-class EscaperTest extends \PHPUnit\Framework\TestCase
+class EscaperTest extends TestCase
 {
     private const TEST_TO_FIXTURE = [
         'testEscapeFieldName' => 'field name',
diff --git a/tests/Redis/Command/AbstractCommandTest.php b/tests/Redis/Command/AbstractCommandTest.php
index 02ccf69..85da6bf 100644
--- a/tests/Redis/Command/AbstractCommandTest.php
+++ b/tests/Redis/Command/AbstractCommandTest.php
@@ -24,6 +24,7 @@
 use InvalidArgumentException;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
 use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakeAbstractCommand;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
@@ -33,7 +34,7 @@
  *
  * @internal
  */
-class AbstractCommandTest extends \PHPUnit\Framework\TestCase
+class AbstractCommandTest extends TestCase
 {
     public function testGetRediSearchVersion(): void
     {
diff --git a/tests/Redis/Command/AddFieldOptionTraitTest.php b/tests/Redis/Command/AddFieldOptionTraitTest.php
index 78187d7..118d576 100644
--- a/tests/Redis/Command/AddFieldOptionTraitTest.php
+++ b/tests/Redis/Command/AddFieldOptionTraitTest.php
@@ -24,6 +24,7 @@
 use BadMethodCallException;
 use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakeAddFieldOptionTraitClass1;
 use MacFJA\RediSearch\tests\fixtures\Redis\Command\FakeAddFieldOptionTraitClass2;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait
@@ -39,7 +40,7 @@
  *
  * @internal
  */
-class AddFieldOptionTraitTest extends \PHPUnit\Framework\TestCase
+class AddFieldOptionTraitTest extends TestCase
 {
     public function testWrongParent(): void
     {
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index 43c0c51..837a9cb 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -28,14 +28,12 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\LimitOption
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\SortByOption
- *
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor
  *
  * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
@@ -188,7 +186,7 @@ public function testFullOption(): void
             'VERBATIM',
             'LOAD', 2, '@geo1', '@tag1',
             'GROUPBY', 1, '@text1', 'REDUCE', 'COUNT_DISTINCT', 1, '@user_id', 'REDUCE', 'AVG', 1, '@num1', 'AS', 'average',
-            'APPLY', '@timestamp - (@timestamp % 86400)',  'AS', 'day',
+            'APPLY', '@timestamp - (@timestamp % 86400)', 'AS', 'day',
             'SORTBY', 3, '@text1', '@num1', 'DESC', 'MAX', 10,
             'LIMIT', 12, 35,
             'FILTER', "@name=='foo' && @age < 20",
@@ -221,9 +219,9 @@ public function testApplyOrder(): void
             '@text1:"hello world"',
             'VERBATIM',
             'LOAD', 2, '@geo1', '@tag1',
-            'APPLY', 'split(@tag1)',  'AS', 'tag2',
+            'APPLY', 'split(@tag1)', 'AS', 'tag2',
             'GROUPBY', 1, '@text1', 'REDUCE', 'COUNT_DISTINCT', 1, '@user_id', 'REDUCE', 'AVG', 1, '@num1', 'AS', 'average',
-            'APPLY', '@age - 1',  'AS', 'age',
+            'APPLY', '@age - 1', 'AS', 'age',
             'SORTBY', 3, '@text1', '@num1', 'DESC', 'MAX', 10,
             'LIMIT', 12, 35,
             'FILTER', "@name=='foo' && @age < 20",
diff --git a/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php b/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php
index 50ba656..56f5ae6 100644
--- a/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php
+++ b/tests/Redis/Command/AggregationCommand/ReduceOptionTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Redis\Command\AggregationCommand;
 
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption
@@ -30,7 +31,7 @@
  *
  * @internal
  */
-class ReduceOptionTest extends \PHPUnit\Framework\TestCase
+class ReduceOptionTest extends TestCase
 {
     public function testShorthands(): void
     {
diff --git a/tests/Redis/Command/AliasAddTest.php b/tests/Redis/Command/AliasAddTest.php
index fee17ac..e557619 100644
--- a/tests/Redis/Command/AliasAddTest.php
+++ b/tests/Redis/Command/AliasAddTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\AliasAdd
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/AliasDelTest.php b/tests/Redis/Command/AliasDelTest.php
index 9ac2379..c181d0b 100644
--- a/tests/Redis/Command/AliasDelTest.php
+++ b/tests/Redis/Command/AliasDelTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\AliasDel
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/AliasUpdateTest.php b/tests/Redis/Command/AliasUpdateTest.php
index d9a6596..025ed6a 100644
--- a/tests/Redis/Command/AliasUpdateTest.php
+++ b/tests/Redis/Command/AliasUpdateTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\AliasUpdate
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/AlterTest.php b/tests/Redis/Command/AlterTest.php
index d2a4fd0..78e01f3 100644
--- a/tests/Redis/Command/AlterTest.php
+++ b/tests/Redis/Command/AlterTest.php
@@ -29,7 +29,6 @@
  * @covers \MacFJA\RediSearch\Redis\Command\Alter
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption
- *
  * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php b/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php
index 7923ba2..258f6f3 100644
--- a/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php
+++ b/tests/Redis/Command/CreateCommand/VectorFieldOptionTest.php
@@ -66,7 +66,7 @@ public function testAddFlatAttribute(): void
         $option->setField('vec');
         $option->setAlgorithm(VectorFieldOption::ALGORITHM_FLAT);
         $option->addFlatAttribute(VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_IP);
-        static::assertSame(['vec', 'VECTOR', 'FLAT',  6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP'], $option->render('2.4.0'));
+        static::assertSame(['vec', 'VECTOR', 'FLAT', 6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP'], $option->render('2.4.0'));
 
         $option = new VectorFieldOption();
         $option->setField('vec');
@@ -95,7 +95,7 @@ public function testAddHnswAttribute(): void
         $option->setField('vec');
         $option->setAlgorithm(VectorFieldOption::ALGORITHM_HNSW);
         $option->addHnswAttribute(VectorFieldOption::TYPE_FLOAT32, 10, VectorFieldOption::DISTANCE_METRIC_IP);
-        static::assertSame(['vec', 'VECTOR', 'HNSW',  6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP'], $option->render('2.4.0'));
+        static::assertSame(['vec', 'VECTOR', 'HNSW', 6, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP'], $option->render('2.4.0'));
 
         $option = new VectorFieldOption();
         $option->setField('vec');
@@ -109,7 +109,7 @@ public function testAddHnswAttribute(): void
             500,
             50
         );
-        static::assertSame(['vec', 'VECTOR', 'HNSW',  14, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP', 'INITIAL_CAP', 1000, 'M', 10, 'EF_CONSTRUCTION', 500, 'EF_RUNTIME', 50], $option->render('2.4.0'));
+        static::assertSame(['vec', 'VECTOR', 'HNSW', 14, 'TYPE', 'FLOAT32', 'DIM', 10, 'DISTANCE_METRIC', 'IP', 'INITIAL_CAP', 1000, 'M', 10, 'EF_CONSTRUCTION', 500, 'EF_RUNTIME', 50], $option->render('2.4.0'));
     }
 
     /**
diff --git a/tests/Redis/Command/CreateTest.php b/tests/Redis/Command/CreateTest.php
index ec651b0..92cda82 100644
--- a/tests/Redis/Command/CreateTest.php
+++ b/tests/Redis/Command/CreateTest.php
@@ -70,7 +70,7 @@ public function testGetArguments(): void
 
         static::assertSame([
             'idx',
-            'PREFIX', 2,  'document_', 'doc_',
+            'PREFIX', 2, 'document_', 'doc_',
             'LANGUAGE', 'french',
             'SCHEMA',
             'title', 'TEXT', 'SORTABLE',
diff --git a/tests/Redis/Command/DictAddTest.php b/tests/Redis/Command/DictAddTest.php
index 4920b49..79aaefc 100644
--- a/tests/Redis/Command/DictAddTest.php
+++ b/tests/Redis/Command/DictAddTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\DictAdd
  **
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/DictDelTest.php b/tests/Redis/Command/DictDelTest.php
index a625cbc..20b5a76 100644
--- a/tests/Redis/Command/DictDelTest.php
+++ b/tests/Redis/Command/DictDelTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\DictDel
  **
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/DictDumpTest.php b/tests/Redis/Command/DictDumpTest.php
index 33dc481..1fee15d 100644
--- a/tests/Redis/Command/DictDumpTest.php
+++ b/tests/Redis/Command/DictDumpTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\DictDump
  **
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/DropIndexTest.php b/tests/Redis/Command/DropIndexTest.php
index 88391cd..6e68e3f 100644
--- a/tests/Redis/Command/DropIndexTest.php
+++ b/tests/Redis/Command/DropIndexTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\DropIndex
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/ExplainCliTest.php b/tests/Redis/Command/ExplainCliTest.php
index 9e049cd..f4cfb2f 100644
--- a/tests/Redis/Command/ExplainCliTest.php
+++ b/tests/Redis/Command/ExplainCliTest.php
@@ -27,7 +27,6 @@
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\Explain
- *
  * @covers \MacFJA\RediSearch\Redis\Command\ExplainCli
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/ExplainTest.php b/tests/Redis/Command/ExplainTest.php
index 052575e..74fe20f 100644
--- a/tests/Redis/Command/ExplainTest.php
+++ b/tests/Redis/Command/ExplainTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\Explain
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/InfoTest.php b/tests/Redis/Command/InfoTest.php
index 7e63472..713e0aa 100644
--- a/tests/Redis/Command/InfoTest.php
+++ b/tests/Redis/Command/InfoTest.php
@@ -27,9 +27,7 @@
 
 /**
  * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
- *
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\Info
  **
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/Option/AbstractCommandOptionTest.php b/tests/Redis/Command/Option/AbstractCommandOptionTest.php
index 8f15150..46be7b0 100644
--- a/tests/Redis/Command/Option/AbstractCommandOptionTest.php
+++ b/tests/Redis/Command/Option/AbstractCommandOptionTest.php
@@ -22,13 +22,14 @@
 namespace MacFJA\RediSearch\tests\Redis\Command\Option;
 
 use MacFJA\RediSearch\tests\fixtures\Redis\Command\Option\FakeAbstractCommandOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
  *
  * @internal
  */
-class AbstractCommandOptionTest extends \PHPUnit\Framework\TestCase
+class AbstractCommandOptionTest extends TestCase
 {
     public function testIsCompatible(): void
     {
diff --git a/tests/Redis/Command/Option/CustomValidatorOptionTest.php b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
index 0d2df9f..c1b985a 100644
--- a/tests/Redis/Command/Option/CustomValidatorOptionTest.php
+++ b/tests/Redis/Command/Option/CustomValidatorOptionTest.php
@@ -25,6 +25,7 @@
 use Generator;
 use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption;
 use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use PHPUnit\Framework\TestCase;
 use Respect\Validation\Rules\Length;
 
 /**
@@ -36,7 +37,7 @@
  *
  * @internal
  */
-class CustomValidatorOptionTest extends \PHPUnit\Framework\TestCase
+class CustomValidatorOptionTest extends TestCase
 {
     use BaseOptionTestTrait;
     use TestIsCompatibleTrait;
diff --git a/tests/Redis/Command/ProfileTest.php b/tests/Redis/Command/ProfileTest.php
index a3ac5b1..ae8f4f5 100644
--- a/tests/Redis/Command/ProfileTest.php
+++ b/tests/Redis/Command/ProfileTest.php
@@ -28,7 +28,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\Profile
  * @covers \MacFJA\RediSearch\Redis\Command\ProfileCommand\QueryOption
  *
diff --git a/tests/Redis/Command/SearchCommand/FilterOptionTest.php b/tests/Redis/Command/SearchCommand/FilterOptionTest.php
index 1fcf14b..35ed9d7 100644
--- a/tests/Redis/Command/SearchCommand/FilterOptionTest.php
+++ b/tests/Redis/Command/SearchCommand/FilterOptionTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Redis\Command\SearchCommand;
 
 use MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption
@@ -30,7 +31,7 @@
  *
  * @internal
  */
-class FilterOptionTest extends \PHPUnit\Framework\TestCase
+class FilterOptionTest extends TestCase
 {
     public function testShorthands(): void
     {
diff --git a/tests/Redis/Command/SearchCommand/LimitOptionTest.php b/tests/Redis/Command/SearchCommand/LimitOptionTest.php
index c83d712..65312de 100644
--- a/tests/Redis/Command/SearchCommand/LimitOptionTest.php
+++ b/tests/Redis/Command/SearchCommand/LimitOptionTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Redis\Command\SearchCommand;
 
 use MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
@@ -29,7 +30,7 @@
  *
  * @internal
  */
-class LimitOptionTest extends \PHPUnit\Framework\TestCase
+class LimitOptionTest extends TestCase
 {
     public function testSetOffset(): void
     {
diff --git a/tests/Redis/Command/SearchTest.php b/tests/Redis/Command/SearchTest.php
index cee08f9..f1f6179 100644
--- a/tests/Redis/Command/SearchTest.php
+++ b/tests/Redis/Command/SearchTest.php
@@ -31,18 +31,14 @@
 
 /**
  * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
- *
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\Search
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\FilterOption
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
- *
  * @covers \MacFJA\RediSearch\Redis\Response\PaginatedResponse
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php b/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php
index 84871b1..7cdf8b1 100644
--- a/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php
+++ b/tests/Redis/Command/SpellCheckCommand/TermsOptionTest.php
@@ -22,6 +22,7 @@
 namespace MacFJA\RediSearch\tests\Redis\Command\SpellCheckCommand;
 
 use MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption
@@ -33,7 +34,7 @@
  *
  * @internal
  */
-class TermsOptionTest extends \PHPUnit\Framework\TestCase
+class TermsOptionTest extends TestCase
 {
     public function testFullOptions(): void
     {
diff --git a/tests/Redis/Command/SpellCheckTest.php b/tests/Redis/Command/SpellCheckTest.php
index 6491ae2..8852f60 100644
--- a/tests/Redis/Command/SpellCheckTest.php
+++ b/tests/Redis/Command/SpellCheckTest.php
@@ -28,9 +28,7 @@
 
 /**
  * @covers  \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
- *
  * @covers  \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SpellCheck
  * @covers \MacFJA\RediSearch\Redis\Command\SpellCheckCommand\TermsOption
  **
diff --git a/tests/Redis/Command/SugAddTest.php b/tests/Redis/Command/SugAddTest.php
index 152ad4d..f1d0521 100644
--- a/tests/Redis/Command/SugAddTest.php
+++ b/tests/Redis/Command/SugAddTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SugAdd
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/SugDelTest.php b/tests/Redis/Command/SugDelTest.php
index e449c0b..a7ef274 100644
--- a/tests/Redis/Command/SugDelTest.php
+++ b/tests/Redis/Command/SugDelTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SugDel
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/SugGetTest.php b/tests/Redis/Command/SugGetTest.php
index 2f6fde7..01c32f8 100644
--- a/tests/Redis/Command/SugGetTest.php
+++ b/tests/Redis/Command/SugGetTest.php
@@ -28,7 +28,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
- *
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\SugGet
  *
@@ -73,7 +72,7 @@ public function testParseResponse(): void
         ];
 
         $rawResult = [
-            'hello',  '0.4024922251701355', 'greeting',
+            'hello', '0.4024922251701355', 'greeting',
             'hola', '0.40000000596046448', 'greeting',
         ];
 
diff --git a/tests/Redis/Command/SugLenTest.php b/tests/Redis/Command/SugLenTest.php
index c71ac59..bb93185 100644
--- a/tests/Redis/Command/SugLenTest.php
+++ b/tests/Redis/Command/SugLenTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SugLen
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/SynDumpTest.php b/tests/Redis/Command/SynDumpTest.php
index 994f9e3..88a6d21 100644
--- a/tests/Redis/Command/SynDumpTest.php
+++ b/tests/Redis/Command/SynDumpTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SynDump
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/Command/SynUpdateTest.php b/tests/Redis/Command/SynUpdateTest.php
index a7a851d..837bdff 100644
--- a/tests/Redis/Command/SynUpdateTest.php
+++ b/tests/Redis/Command/SynUpdateTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\SynUpdate
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
@@ -54,7 +53,7 @@ public function testFullOption(): void
         ;
 
         static::assertSame([
-            'idx', 'blue', 'SKIPINITIALSCAN', 'blue',  'cyan', 'navy', 'teal',
+            'idx', 'blue', 'SKIPINITIALSCAN', 'blue', 'cyan', 'navy', 'teal',
         ], $command->getArguments());
     }
 }
diff --git a/tests/Redis/Command/TagValsTest.php b/tests/Redis/Command/TagValsTest.php
index 3a0db8d..a2f650e 100644
--- a/tests/Redis/Command/TagValsTest.php
+++ b/tests/Redis/Command/TagValsTest.php
@@ -26,7 +26,6 @@
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
- *
  * @covers \MacFJA\RediSearch\Redis\Command\TagVals
  *
  * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
diff --git a/tests/Redis/InitializerTest.php b/tests/Redis/InitializerTest.php
index 8cde0b2..163e517 100644
--- a/tests/Redis/InitializerTest.php
+++ b/tests/Redis/InitializerTest.php
@@ -24,16 +24,18 @@
 use Generator;
 use MacFJA\RediSearch\Redis\Client;
 use MacFJA\RediSearch\Redis\Initializer;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @covers \MacFJA\RediSearch\Redis\Initializer
  *
  * @internal
  */
-class InitializerTest extends \PHPUnit\Framework\TestCase
+class InitializerTest extends TestCase
 {
     /**
      * @param array<string> $input
+     *
      * @dataProvider dataProvider
      */
     public function testRediSearchVersion(array $input, ?string $expected): void
diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php
index 376f8c3..fb4dc9c 100644
--- a/tests/integration/DockerTest.php
+++ b/tests/integration/DockerTest.php
@@ -173,6 +173,7 @@ public function testGetClient(string $expectedClass, $redisBuilder): void
 
     /**
      * @dataProvider dataProvider
+     *
      * @large
      *
      * @param mixed $clientBuilder
@@ -299,6 +300,7 @@ public function dataProvider(string $testName): array
 
     /**
      * @dataProvider dataProvider
+     *
      * @large
      *
      * @param mixed $clientBuilder

From be07128e4e2dad1d0005800d37fceb891ca4bcea Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Wed, 19 Oct 2022 18:24:09 +0200
Subject: [PATCH 58/60] Add JSONPath validation

---
 CHANGELOG.md                                  |  4 +
 composer.json                                 |  3 +-
 src/Exception/InvalidJSONPathException.php    | 33 +++++++++
 .../Command/CreateCommand/JSONFieldOption.php | 28 +++++++
 .../CreateCommand/JSONFieldOptionTest.php     | 74 +++++++++++++++++++
 .../Command/Option/JSONFieldOptionTest.php    | 11 ++-
 6 files changed, 150 insertions(+), 3 deletions(-)
 create mode 100644 src/Exception/InvalidJSONPathException.php
 create mode 100644 tests/Redis/Command/CreateCommand/JSONFieldOptionTest.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15e571f..165cd4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased v2.x]
 
+### Added
+
+- Path validation for JSON fields
+
 ### Changed
 
 - (dev) Update PHP-CS-Fixer to version `3.12.0`
diff --git a/composer.json b/composer.json
index 145cdce..03926c5 100644
--- a/composer.json
+++ b/composer.json
@@ -23,7 +23,8 @@
         "php": "^7.2 || ^8.0",
         "ext-intl": "*",
         "composer/semver": "^3.2",
-        "respect/validation": "^2.0"
+        "respect/validation": "^2.0",
+        "softcreatr/jsonpath": "^0.7.6 || ^0.8.1"
     },
     "require-dev": {
         "ext-mbstring": "*",
diff --git a/src/Exception/InvalidJSONPathException.php b/src/Exception/InvalidJSONPathException.php
new file mode 100644
index 0000000..e4eba00
--- /dev/null
+++ b/src/Exception/InvalidJSONPathException.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\Exception;
+
+use Exception;
+use InvalidArgumentException;
+
+class InvalidJSONPathException extends InvalidArgumentException
+{
+    public function __construct(string $path, Exception $previous = null)
+    {
+        parent::__construct(sprintf('The provided JSON Path is not valid. (path: "%s")', $path), 0, $previous);
+    }
+}
diff --git a/src/Redis/Command/CreateCommand/JSONFieldOption.php b/src/Redis/Command/CreateCommand/JSONFieldOption.php
index 9b2588b..263b16c 100644
--- a/src/Redis/Command/CreateCommand/JSONFieldOption.php
+++ b/src/Redis/Command/CreateCommand/JSONFieldOption.php
@@ -21,6 +21,11 @@
 
 namespace MacFJA\RediSearch\Redis\Command\CreateCommand;
 
+use function count;
+
+use Flow\JSONPath\JSONPath;
+use Flow\JSONPath\JSONPathException;
+use MacFJA\RediSearch\Exception\InvalidJSONPathException;
 use MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption;
 
 class JSONFieldOption extends AbstractCommandOption implements CreateCommandJSONFieldOption
@@ -36,6 +41,9 @@ public function __construct(string $path, CreateCommandFieldOption $decorated)
         parent::__construct('>=2.2.0');
         $this->path = $path;
         $this->decorated = $decorated;
+        if (!self::isPathValid($path)) {
+            throw new InvalidJSONPathException($path);
+        }
     }
 
     public function isValid(): bool
@@ -63,6 +71,26 @@ public function getJSONPath(): string
         return $this->path;
     }
 
+    /**
+     * @throws InvalidJSONPathException
+     */
+    public static function isPathValid(string $path): bool
+    {
+        if (0 === strpos($path, '$')) {
+            $parser = new JSONPath();
+
+            try {
+                $result = $parser->parseTokens($path);
+
+                return count($result) > 0;
+            } catch (JSONPathException $e) {
+                throw new InvalidJSONPathException($path, $e);
+            }
+        }
+
+        return false;
+    }
+
     protected function doRender(?string $version): array
     {
         return array_merge([$this->path, 'AS'], $this->decorated->render($version));
diff --git a/tests/Redis/Command/CreateCommand/JSONFieldOptionTest.php b/tests/Redis/Command/CreateCommand/JSONFieldOptionTest.php
new file mode 100644
index 0000000..e78a54f
--- /dev/null
+++ b/tests/Redis/Command/CreateCommand/JSONFieldOptionTest.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\CreateCommand;
+
+use MacFJA\RediSearch\Exception\InvalidJSONPathException;
+use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @internal
+ *
+ * @covers \MacFJA\RediSearch\Exception\InvalidJSONPathException
+ * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption
+ */
+class JSONFieldOptionTest extends TestCase
+{
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testIsPathValid(string $input, bool $expected): void
+    {
+        static::assertEquals($expected, JSONFieldOption::isPathValid($input));
+    }
+
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testIsPathValidException(string $input, bool $expected): void
+    {
+        $this->expectException(InvalidJSONPathException::class);
+        static::assertEquals($expected, JSONFieldOption::isPathValid($input));
+    }
+
+    /**
+     * @return array<string, array<bool|string>>
+     */
+    public function dataProvider(string $testName): array
+    {
+        if ('testIsPathValid' === $testName) {
+            return [
+                '2nd element of root' => ['$.[1]', true],
+                '2nd element' => ['$[1]', true],
+                'multilevel' => ['$.basic.dict.new_child_1', true],
+                'empty 2' => [' ', false],
+                'empty' => ['', false],
+                'not close 2' => ['$[', false],
+                'missing dollar' => ['.basic', false],
+            ];
+        }
+
+        return [
+            'not close 1' => ['$(', false],
+        ];
+    }
+}
diff --git a/tests/Redis/Command/Option/JSONFieldOptionTest.php b/tests/Redis/Command/Option/JSONFieldOptionTest.php
index 90ba780..ddaf873 100644
--- a/tests/Redis/Command/Option/JSONFieldOptionTest.php
+++ b/tests/Redis/Command/Option/JSONFieldOptionTest.php
@@ -21,6 +21,7 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command\Option;
 
+use MacFJA\RediSearch\Exception\InvalidJSONPathException;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption;
 use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption;
 use PHPUnit\Framework\TestCase;
@@ -36,6 +37,7 @@
  * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
  * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Exception\InvalidJSONPathException
  *
  * @internal
  */
@@ -50,9 +52,14 @@ public function testIsValid(): void
         static::assertFalse($json->isValid());
 
         $option->setField('data');
-        $json = new JSONFieldOption('', $option);
         static::assertTrue($option->isValid());
-        static::assertFalse($json->isValid());
+
+        try {
+            new JSONFieldOption('', $option);
+            static::fail();
+        } catch (InvalidJSONPathException $e) {
+            // Do nothing
+        }
 
         $json = new JSONFieldOption('$.data', $option);
         static::assertTrue($option->isValid());

From f87998a1eeb9ea29f887bbe16c971cad8734b845 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Wed, 19 Oct 2022 18:26:34 +0200
Subject: [PATCH 59/60] Add more unit tests

---
 CHANGELOG.md                                  |  2 +
 src/Redis/Initializer.php                     | 18 ++++-
 tests/Redis/Command/AggregateTest.php         |  9 +++
 .../WithPublicGroupedSetterTraitTest.php      | 80 +++++++++++++++++++
 .../ProfileCommand/QueryOptionTest.php        | 70 ++++++++++++++++
 tests/Redis/InitializerTest.php               | 28 +++++++
 .../Option/FakeWithPublicGroupedSetter.php    | 43 ++++++++++
 ...FakeWithPublicGroupedSetterInvalidSelf.php | 40 ++++++++++
 8 files changed, 286 insertions(+), 4 deletions(-)
 create mode 100644 tests/Redis/Command/Option/WithPublicGroupedSetterTraitTest.php
 create mode 100644 tests/Redis/Command/ProfileCommand/QueryOptionTest.php
 create mode 100644 tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetter.php
 create mode 100644 tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetterInvalidSelf.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 165cd4f..9cf22d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Path validation for JSON fields
+- Helper function get current RedisJSON version + (abstraction)
+- (dev) More unit tests
 
 ### Changed
 
diff --git a/src/Redis/Initializer.php b/src/Redis/Initializer.php
index bc39538..c45fcbb 100644
--- a/src/Redis/Initializer.php
+++ b/src/Redis/Initializer.php
@@ -139,7 +139,7 @@ public static function registerCommandsRediska(): void
         Rediska_Commands::add('__redisearch', RediskaRediSearchCommand::class);
     }
 
-    public static function getRediSearchVersion(Client $client): ?string
+    public static function getRedisModuleVersion(Client $client, string $module): ?string
     {
         try {
             $modules = $client->executeRaw('module', 'list') ?? [];
@@ -151,17 +151,17 @@ public static function getRediSearchVersion(Client $client): ?string
             return null;
         }
 
-        foreach ($modules as $module) {
+        foreach ($modules as $moduleData) {
             $data = array_column(
                 array_chunk(
-                    $module,
+                    $moduleData,
                     2
                 ),
                 1,
                 0
             );
 
-            if (!(($data['name'] ?? '') === 'search') || empty($data['ver'])) {
+            if (!(($data['name'] ?? '') === $module) || empty($data['ver'])) {
                 continue;
             }
 
@@ -179,4 +179,14 @@ public static function getRediSearchVersion(Client $client): ?string
 
         return null;
     }
+
+    public static function getRediSearchVersion(Client $client): ?string
+    {
+        return self::getRedisModuleVersion($client, 'search');
+    }
+
+    public static function getRedisJSONVersion(Client $client): ?string
+    {
+        return self::getRedisModuleVersion($client, 'ReJSON');
+    }
 }
diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php
index 837a9cb..45a840f 100644
--- a/tests/Redis/Command/AggregateTest.php
+++ b/tests/Redis/Command/AggregateTest.php
@@ -21,12 +21,14 @@
 
 namespace MacFJA\RediSearch\tests\Redis\Command;
 
+use MacFJA\RediSearch\Exception\UnexpectedServerResponseException;
 use MacFJA\RediSearch\Redis\Command\Aggregate;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\GroupByOption;
 use MacFJA\RediSearch\Redis\Command\AggregateCommand\ReduceOption;
 use PHPUnit\Framework\TestCase;
 
 /**
+ * @covers \MacFJA\RediSearch\Exception\UnexpectedServerResponseException
  * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand
  * @covers \MacFJA\RediSearch\Redis\Command\Aggregate
  * @covers \MacFJA\RediSearch\Redis\Command\AggregateCommand\ApplyOption
@@ -228,4 +230,11 @@ public function testApplyOrder(): void
             'WITHCURSOR', 'COUNT', 20, 'MAXIDLE', 30,
         ], $command->getArguments());
     }
+
+    public function testResponseError(): void
+    {
+        $this->expectException(UnexpectedServerResponseException::class);
+        $command = new Aggregate();
+        $command->parseResponse(false);
+    }
 }
diff --git a/tests/Redis/Command/Option/WithPublicGroupedSetterTraitTest.php b/tests/Redis/Command/Option/WithPublicGroupedSetterTraitTest.php
new file mode 100644
index 0000000..12abf01
--- /dev/null
+++ b/tests/Redis/Command/Option/WithPublicGroupedSetterTraitTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\Option;
+
+use BadMethodCallException;
+use InvalidArgumentException;
+use MacFJA\RediSearch\Redis\Command\Option\NamelessOption;
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\Option\FakeWithPublicGroupedSetter;
+use MacFJA\RediSearch\tests\fixtures\Redis\Command\Option\FakeWithPublicGroupedSetterInvalidSelf;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ *
+ * @internal
+ */
+class WithPublicGroupedSetterTraitTest extends TestCase
+{
+    public function testInvalidSelf(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('This method is not callable in '.FakeWithPublicGroupedSetterInvalidSelf::class);
+        $class = new FakeWithPublicGroupedSetterInvalidSelf();
+        $class->setName('John');
+    }
+
+    public function testInvalidSetterName(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('Call undefined method setAge in '.FakeWithPublicGroupedSetter::class);
+        $class = new FakeWithPublicGroupedSetter([], []);
+        $class->setAge(50);
+    }
+
+    public function testInvalidSetterArgCount(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('The method '.FakeWithPublicGroupedSetter::class.'::setName need exactly one argument');
+        $class = new FakeWithPublicGroupedSetter([], []);
+        $class->setName('John', 'Doe');
+    }
+
+    public function testUndefinedMethod(): void
+    {
+        $this->expectException(BadMethodCallException::class);
+        $this->expectExceptionMessage('Call undefined method foo in '.FakeWithPublicGroupedSetter::class);
+        $class = new FakeWithPublicGroupedSetter([], []);
+        $class->foo();
+    }
+
+    public function testSetter(): void
+    {
+        $class = new FakeWithPublicGroupedSetter(['name' => new NamelessOption()], []);
+        $class->setName('John');
+        static::assertSame('John', $class->getDataOfOption('name'));
+    }
+}
diff --git a/tests/Redis/Command/ProfileCommand/QueryOptionTest.php b/tests/Redis/Command/ProfileCommand/QueryOptionTest.php
new file mode 100644
index 0000000..32c93fa
--- /dev/null
+++ b/tests/Redis/Command/ProfileCommand/QueryOptionTest.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\Redis\Command\ProfileCommand;
+
+use MacFJA\RediSearch\Redis\Command\Aggregate;
+use MacFJA\RediSearch\Redis\Command\ProfileCommand\QueryOption;
+use MacFJA\RediSearch\Redis\Command\Search;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \MacFJA\RediSearch\Redis\Command\ProfileCommand\QueryOption
+ *
+ * @uses \MacFJA\RediSearch\Redis\Command\Search
+ * @uses \MacFJA\RediSearch\Redis\Command\Aggregate
+ * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionTrait
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NotEmptyOption
+ * @uses \MacFJA\RediSearch\Redis\Command\Option\NumberedOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\HighlightOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SortByOption
+ * @uses \MacFJA\RediSearch\Redis\Command\SearchCommand\SummarizeOption
+ * @uses \MacFJA\RediSearch\Redis\Command\AggregateCommand\WithCursor
+ *
+ * @internal
+ */
+class QueryOptionTest extends TestCase
+{
+    public function testGetOptionData(): void
+    {
+        $command = (new Search())->setIndex('idx')->setQuery('*');
+        static::assertSame([
+            'command' => $command,
+        ], (new QueryOption())->setCommand($command)->getOptionData());
+    }
+
+    public function testIsSearch(): void
+    {
+        $search = (new Search())->setIndex('idx')->setQuery('*');
+        $aggregate = (new Aggregate())->setIndex('idx')->setQuery('*');
+        static::assertTrue((new QueryOption())->setCommand($search)->isSearch());
+        static::assertFalse((new QueryOption())->setCommand($aggregate)->isSearch());
+    }
+}
diff --git a/tests/Redis/InitializerTest.php b/tests/Redis/InitializerTest.php
index 163e517..700d94d 100644
--- a/tests/Redis/InitializerTest.php
+++ b/tests/Redis/InitializerTest.php
@@ -46,6 +46,19 @@ public function testRediSearchVersion(array $input, ?string $expected): void
         static::assertSame($expected, Initializer::getRediSearchVersion($client));
     }
 
+    /**
+     * @param array<string> $input
+     *
+     * @dataProvider dataProvider
+     */
+    public function testRedisJSONVersion(array $input, ?string $expected): void
+    {
+        $client = $this->createMock(Client::class);
+        $client->method('executeRaw')->with('module', 'list')->willReturn($input);
+
+        static::assertSame($expected, Initializer::getRedisJSONVersion($client));
+    }
+
     /**
      * @return Generator<array>
      */
@@ -66,5 +79,20 @@ public function dataProvider(string $testName): Generator
 
             yield [[['name', 'json', 'ver', '99999'], ['name', 'search', 'ver', '20005']], '2.0.5'];
         }
+        if ('testRedisJSONVersion' === $testName) {
+            yield [[['name', 'ReJSON', 'ver', '20005']], '2.0.5'];
+
+            yield [[['name', 'ReJSON', 'ver', '20000']], '2.0.0'];
+
+            yield [[['name', 'ReJSON', 'ver', '26512']], '2.65.12'];
+
+            yield [[['name', 'ReJSON', 'ver', '120569']], '12.5.69'];
+
+            yield [[['name', 'ReJSON', 'ver', '99999']], '9.99.99'];
+
+            yield [[['name', 'json', 'ver', '99999']], null];
+
+            yield [[['name', 'json', 'ver', '99999'], ['name', 'ReJSON', 'ver', '20005']], '2.0.5'];
+        }
     }
 }
diff --git a/tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetter.php b/tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetter.php
new file mode 100644
index 0000000..6dc7073
--- /dev/null
+++ b/tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetter.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command\Option;
+
+use MacFJA\RediSearch\Redis\Command\Option\GroupedOption;
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+
+/**
+ * @method void setName(string $name, $arg = null)
+ * @method void setAge(int $age)                   Fake setter to please PHPStan
+ * @method void foo()                              Fake method to please PHPStan
+ */
+class FakeWithPublicGroupedSetter extends GroupedOption
+{
+    use WithPublicGroupedSetterTrait;
+
+    /**
+     * @return string[]
+     */
+    protected function publicSetter(): array
+    {
+        return ['name'];
+    }
+}
diff --git a/tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetterInvalidSelf.php b/tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetterInvalidSelf.php
new file mode 100644
index 0000000..51e6dd7
--- /dev/null
+++ b/tests/fixtures/Redis/Command/Option/FakeWithPublicGroupedSetterInvalidSelf.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * Copyright MacFJA
+ *
+ * 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.
+ */
+
+namespace MacFJA\RediSearch\tests\fixtures\Redis\Command\Option;
+
+use MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait;
+
+/**
+ * @method void setName(string $name)
+ */
+class FakeWithPublicGroupedSetterInvalidSelf
+{
+    use WithPublicGroupedSetterTrait;
+
+    /**
+     * @return array<string>
+     */
+    protected function publicSetter(): array
+    {
+        return [];
+    }
+}

From be245905cecd5aaf1a6a16fe0796e0c772e1bdc7 Mon Sep 17 00:00:00 2001
From: MacFJA <macfja@gmail.com>
Date: Mon, 6 Nov 2023 22:19:53 +0100
Subject: [PATCH 60/60] Fix deprecation warning with PHP 8.1+

---
 src/Query/Builder/QueryElementDecorator.php   |  4 +-
 src/Query/Builder/QueryElementVector.php      |  2 +-
 src/Redis/Client/PhpredisClient.php           |  1 +
 .../Command/AggregateCommand/LimitOption.php  |  4 +-
 .../CreateCommand/VectorFieldOption.php       |  3 +-
 .../Command/Option/DecoratedOptionTrait.php   |  3 +
 src/Redis/Command/Search.php                  |  3 +-
 src/Redis/Response.php                        |  4 +-
 src/Redis/Response/AggregateResponseItem.php  |  1 +
 src/Redis/Response/PaginatedResponse.php      |  8 +--
 tests/Query/BuilderTest.php                   |  3 +-
 .../Command/Option/GroupedOptionTest.php      |  4 +-
 tests/psalm/stubs/phpiredis.php               | 72 +++++--------------
 13 files changed, 36 insertions(+), 76 deletions(-)

diff --git a/src/Query/Builder/QueryElementDecorator.php b/src/Query/Builder/QueryElementDecorator.php
index 4208ad9..43ea0fe 100644
--- a/src/Query/Builder/QueryElementDecorator.php
+++ b/src/Query/Builder/QueryElementDecorator.php
@@ -21,6 +21,4 @@
 
 namespace MacFJA\RediSearch\Query\Builder;
 
-interface QueryElementDecorator extends QueryElement
-{
-}
+interface QueryElementDecorator extends QueryElement {}
diff --git a/src/Query/Builder/QueryElementVector.php b/src/Query/Builder/QueryElementVector.php
index 6b55c2d..a09ff78 100644
--- a/src/Query/Builder/QueryElementVector.php
+++ b/src/Query/Builder/QueryElementVector.php
@@ -33,7 +33,7 @@
 class QueryElementVector implements QueryElementDecorator
 {
     private const PARAMETERS = [
-        'EF_RUNTIME' => 'int',
+        'EF_RUNTIME' => 'integer',
     ];
 
     /** @var QueryElement */
diff --git a/src/Redis/Client/PhpredisClient.php b/src/Redis/Client/PhpredisClient.php
index d45001b..09bac00 100644
--- a/src/Redis/Client/PhpredisClient.php
+++ b/src/Redis/Client/PhpredisClient.php
@@ -85,6 +85,7 @@ public function executeRaw(...$args)
         if (count($args) < 1) {
             return null;
         }
+
         // @phpstan-ignore-next-line
         return $this->redis->rawCommand(...$args);
     }
diff --git a/src/Redis/Command/AggregateCommand/LimitOption.php b/src/Redis/Command/AggregateCommand/LimitOption.php
index 7412a6f..8d22d64 100644
--- a/src/Redis/Command/AggregateCommand/LimitOption.php
+++ b/src/Redis/Command/AggregateCommand/LimitOption.php
@@ -21,6 +21,4 @@
 
 namespace MacFJA\RediSearch\Redis\Command\AggregateCommand;
 
-class LimitOption extends \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption
-{
-}
+class LimitOption extends \MacFJA\RediSearch\Redis\Command\SearchCommand\LimitOption {}
diff --git a/src/Redis/Command/CreateCommand/VectorFieldOption.php b/src/Redis/Command/CreateCommand/VectorFieldOption.php
index ecdd146..f43d94c 100644
--- a/src/Redis/Command/CreateCommand/VectorFieldOption.php
+++ b/src/Redis/Command/CreateCommand/VectorFieldOption.php
@@ -121,8 +121,7 @@ public function addAttribute(string $type, int $dimension, string $distanceMetri
             + (is_int($blockSize) ? 2 : 0)
             + (is_int($maxEdge) ? 2 : 0)
             + (is_int($efConstruction) ? 2 : 0)
-            + (is_int($efRuntime) ? 2 : 0)
-        ;
+            + (is_int($efRuntime) ? 2 : 0);
 
         /** @var int $count */
         $count = $this->getDataOfOption('count') ?? 0;
diff --git a/src/Redis/Command/Option/DecoratedOptionTrait.php b/src/Redis/Command/Option/DecoratedOptionTrait.php
index b84ec8a..5c3323e 100644
--- a/src/Redis/Command/Option/DecoratedOptionTrait.php
+++ b/src/Redis/Command/Option/DecoratedOptionTrait.php
@@ -61,6 +61,9 @@ public function getVersionConstraint(): string
         return $this->decorated->getVersionConstraint();
     }
 
+    /**
+     * @return array<float|int|string>
+     */
     public function render(?string $version = null): array
     {
         return $this->decorated->render($version);
diff --git a/src/Redis/Command/Search.php b/src/Redis/Command/Search.php
index 7aa9639..7fcfe26 100644
--- a/src/Redis/Command/Search.php
+++ b/src/Redis/Command/Search.php
@@ -360,8 +360,7 @@ public function parseResponse($data)
             + (true === $useScores ? 1 : 0)
             + (true === $usePayloads ? 1 : 0)
             + (true === $useSortKeys ? 1 : 0)
-            - (true === $noContent ? 1 : 0)
-        ;
+            - (true === $noContent ? 1 : 0);
 
         $documents = array_chunk($data, $chunkSize);
 
diff --git a/src/Redis/Response.php b/src/Redis/Response.php
index a4b7a5c..e037c7c 100644
--- a/src/Redis/Response.php
+++ b/src/Redis/Response.php
@@ -21,6 +21,4 @@
 
 namespace MacFJA\RediSearch\Redis;
 
-interface Response
-{
-}
+interface Response {}
diff --git a/src/Redis/Response/AggregateResponseItem.php b/src/Redis/Response/AggregateResponseItem.php
index 9c18f12..f9c365b 100644
--- a/src/Redis/Response/AggregateResponseItem.php
+++ b/src/Redis/Response/AggregateResponseItem.php
@@ -65,6 +65,7 @@ public function getValue(string $fieldName, $default = null)
             __METHOD__,
             __CLASS__
         ), E_USER_DEPRECATED);
+
         // @phpstan-ignore-next-line
         return $this->getFieldValue($fieldName, $default);
     }
diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php
index 126aa30..0e14dec 100644
--- a/src/Redis/Response/PaginatedResponse.php
+++ b/src/Redis/Response/PaginatedResponse.php
@@ -66,7 +66,7 @@ public function __construct(PaginatedCommand $command, int $totalCount, array $i
     /**
      * @return array<ResponseItem>
      */
-    public function current()
+    public function current(): array
     {
         if ($this->requestedOffset === ($this->lastCommand->getOffset() ?? 0) && ($this->requestedSize === $this->getPageSize())) {
             $this->requestedOffset = null;
@@ -101,7 +101,7 @@ public function getPageCount(): int
         return (int) ceil($this->totalCount / $this->getPageSize());
     }
 
-    public function key()
+    public function key(): int
     {
         if (0 === $this->getPageSize()) {
             return 0;
@@ -110,7 +110,7 @@ public function key()
         return (int) floor(($this->lastCommand->getOffset() ?? $this->requestedOffset ?? 0) / $this->getPageSize());
     }
 
-    public function valid()
+    public function valid(): bool
     {
         return (
             null === $this->requestedOffset
@@ -133,7 +133,7 @@ public function getTotalCount(): int
         return $this->totalCount;
     }
 
-    public function count()
+    public function count(): int
     {
         return $this->getPageCount();
     }
diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php
index 625f878..6436fb4 100644
--- a/tests/Query/BuilderTest.php
+++ b/tests/Query/BuilderTest.php
@@ -384,8 +384,7 @@ public function testDocExample3(): void
         $expected = 'hello|world';
         $builder = new Builder();
 
-        $builder->addElement(Builder\OrGroup::fromStrings('hello', 'world'))
-        ;
+        $builder->addElement(Builder\OrGroup::fromStrings('hello', 'world'));
 
         static::assertSame($expected, $builder->render());
     }
diff --git a/tests/Redis/Command/Option/GroupedOptionTest.php b/tests/Redis/Command/Option/GroupedOptionTest.php
index 71033c2..f05ca4c 100644
--- a/tests/Redis/Command/Option/GroupedOptionTest.php
+++ b/tests/Redis/Command/Option/GroupedOptionTest.php
@@ -107,7 +107,7 @@ public function testGetOptionsName(): void
     public function testSetUnknownOptionData(): void
     {
         $this->expectException(OutOfRangeException::class);
-        $this->expectErrorMessage('There is no option for foobar');
+        $this->expectExceptionMessage('There is no option for foobar');
 
         $option = new GroupedOption([], []);
         $option->setDataOfOption('foobar', 'bar');
@@ -116,7 +116,7 @@ public function testSetUnknownOptionData(): void
     public function testSetLockOptionData(): void
     {
         $this->expectException(BadMethodCallException::class);
-        $this->expectErrorMessage('The option lock can\'t be modified');
+        $this->expectExceptionMessage('The option lock can\'t be modified');
 
         $option = new GroupedOption(['lock' => new NamelessOption('foo')], [], ['lock']);
         $option->setDataOfOption('lock', 'bar');
diff --git a/tests/psalm/stubs/phpiredis.php b/tests/psalm/stubs/phpiredis.php
index 87cb532..8bd01f2 100644
--- a/tests/psalm/stubs/phpiredis.php
+++ b/tests/psalm/stubs/phpiredis.php
@@ -30,15 +30,9 @@
 const PHPIREDIS_REPLY_STATUS = 5;
 const PHPIREDIS_REPLY_ERROR = 6;
 
-function phpiredis_connect($host, $port = 6379): void
-{
-}
-function phpiredis_pconnect($host, $port = 6379): void
-{
-}
-function phpiredis_disconnect(): void
-{
-}
+function phpiredis_connect($host, $port = 6379): void {}
+function phpiredis_pconnect($host, $port = 6379): void {}
+function phpiredis_disconnect(): void {}
 
 /**
  * @param resource $redis
@@ -46,18 +40,14 @@ function phpiredis_disconnect(): void
  *
  * @return bool|string
  */
-function phpiredis_command_bs($redis, array $command)
-{
-}
+function phpiredis_command_bs($redis, array $command) {}
 
 /**
  * @param resource $redis
  *
  * @return bool|string
  */
-function phpiredis_command($redis, string $command)
-{
-}
+function phpiredis_command($redis, string $command) {}
 
 /**
  * @param resource      $redis
@@ -65,9 +55,7 @@ function phpiredis_command($redis, string $command)
  *
  * @return array<bool|string>
  */
-function phpiredis_multi_command($redis, array $commands)
-{
-}
+function phpiredis_multi_command($redis, array $commands) {}
 
 /**
  * @param resource             $redis
@@ -75,85 +63,61 @@ function phpiredis_multi_command($redis, array $commands)
  *
  * @return array<bool|string>
  */
-function phpiredis_multi_command_bs($redis, array $commands)
-{
-}
+function phpiredis_multi_command_bs($redis, array $commands) {}
 
 /**
  * @param array<float|int|string> $args
  */
-function phpiredis_format_command(array $args): string
-{
-}
+function phpiredis_format_command(array $args): string {}
 
 /**
  * @return resource
  */
-function phpiredis_reader_create()
-{
-}
+function phpiredis_reader_create() {}
 
 /**
  * @param resource $reader
  */
-function phpiredis_reader_reset($reader): void
-{
-}
+function phpiredis_reader_reset($reader): void {}
 
 /**
  * @param resource $reader
  */
-function phpiredis_reader_feed($reader, string $response): void
-{
-}
+function phpiredis_reader_feed($reader, string $response): void {}
 
 /**
  * @param resource $reader
  *
  * @return bool|int
  */
-function phpiredis_reader_get_state($reader)
-{
-}
+function phpiredis_reader_get_state($reader) {}
 
 /**
  * @param resource $reader
  *
  * @return bool|string
  */
-function phpiredis_reader_get_error($reader)
-{
-}
+function phpiredis_reader_get_error($reader) {}
 
 /**
  * @param resource $reader
  *
  * @return array<array|bool|int|string>|bool|int|string
  */
-function phpiredis_reader_get_reply($reader, int &$type)
-{
-}
+function phpiredis_reader_get_reply($reader, int &$type) {}
 
 /**
  * @param resource $redis
  */
-function phpiredis_reader_destroy($redis): void
-{
-}
+function phpiredis_reader_destroy($redis): void {}
 
 /**
  * @param resource $reader
  */
-function phpiredis_reader_set_error_handler($reader, ?callable $handler): void
-{
-}
+function phpiredis_reader_set_error_handler($reader, ?callable $handler): void {}
 
 /**
  * @param resource $reader
  */
-function phpiredis_reader_set_status_handler($reader, ?callable $handler): void
-{
-}
-function phpiredis_utils_crc16(string $data): int
-{
-}
+function phpiredis_reader_set_status_handler($reader, ?callable $handler): void {}
+function phpiredis_utils_crc16(string $data): int {}
