├── .coveralls.yml ├── .distignore ├── .dockerignore ├── .env.dist ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── code-quality.yml │ ├── code-standard.yml │ ├── integration-testing.yml │ ├── schema-linter.yml │ ├── upload-release.yml │ └── upload-schema-artifact.yml ├── .gitignore ├── .phpcs.xml.dist ├── CHANGELOG.md ├── LICENSE ├── README.md ├── access-functions.php ├── bin ├── _env.sh ├── _lib.sh ├── install-stan-env.sh ├── install-test-env.sh ├── run-docker.sh └── wp-cli.yml ├── codeception.dist.yml ├── composer.json ├── composer.lock ├── docker-compose.yml ├── docker ├── app.Dockerfile ├── app.entrypoint.sh ├── app.post-setup.sh ├── app.setup.sh ├── testing.Dockerfile └── testing.entrypoint.sh ├── logo.png ├── phpstan.neon.dist ├── phpstan ├── class-facetwp-facet.php ├── class-facetwp.php ├── class-wp-post-type.php └── constants.php ├── readme.txt ├── src ├── Autoloader.php ├── CoreSchemaFilters.php ├── Main.php ├── Registry │ ├── FacetRegistry.php │ └── TypeRegistry.php └── Type │ ├── Enum │ ├── ProximityRadiusOptions.php │ └── SortOptionsEnum.php │ ├── Input │ ├── DateRangeArgs.php │ ├── NumberRangeArgs.php │ ├── ProximityArgs.php │ └── SliderArgs.php │ ├── WPInterface │ └── FacetConfig.php │ └── WPObject │ ├── Facet.php │ ├── FacetChoice.php │ ├── FacetPager.php │ ├── FacetRangeSettings.php │ ├── FacetSettings.php │ ├── FacetSortOptionOrderBySetting.php │ └── FacetSortOptionSetting.php ├── tests ├── .gitignore ├── _data │ ├── .gitignore │ ├── _env │ │ └── docker.yml │ └── config.php ├── _output │ └── .gitignore ├── _support │ ├── AcceptanceTester.php │ ├── Helper │ │ ├── Acceptance.php │ │ └── Wpunit.php │ ├── TestCase │ │ ├── FWPGraphQLTestCase.php │ │ └── FacetTestCase.php │ ├── WpunitTester.php │ └── _generated │ │ ├── AcceptanceTesterActions.php │ │ └── WpunitTesterActions.php ├── acceptance.suite.dist.yml ├── acceptance │ └── ActivationCest.php ├── bootstrap.php ├── wpunit.suite.dist.yml └── wpunit │ ├── AccessFunctionsTest.php │ ├── MainTest.php │ └── SortFacetTest.php └── wp-graphql-facetwp.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: php-coveralls 2 | coverage_clover: tests/_output/coverage.xml 3 | json_path: tests/_output/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | /.devcontainer 2 | /.git 3 | /.github 4 | /.idea 5 | /.log 6 | /.vscode 7 | /.wordpress-org 8 | /bin 9 | /docker 10 | /docker-output 11 | /docs 12 | /phpstan 13 | /tests 14 | 15 | .coveralls.yml 16 | .distignore 17 | .dockerignore 18 | .DS_Store 19 | .env 20 | .env.dist 21 | .gitignore 22 | .phpcs.xml 23 | .phpcs.xml.dist 24 | CHANGELOG.md 25 | codeception.dist.yml 26 | codeception.yml 27 | composer.json 28 | composer.lock 29 | docker-compose.yml 30 | phpstan.neon.dist 31 | phpstan.neon 32 | README.md 33 | schema.graphql 34 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # The following files should be ignored: 2 | # - unencrypted sensitive data 3 | # - files that are not checked into the code repository 4 | # - files that are not relevant to the Docker build 5 | 6 | .git 7 | .idea 8 | bin 9 | docker-output 10 | docs 11 | img 12 | 13 | tests/_output 14 | !tests/_output/.gitignore 15 | tests/_support/_generated 16 | !tests/_support/_generated/.gitignore 17 | 18 | vendor 19 | !vendor/autoload.php 20 | !vendor/composer 21 | 22 | .dockerignore 23 | .gitignore 24 | .env 25 | CODE_OF_CONDUCT.md 26 | CONTRIBUTING.md 27 | Dockerfile* 28 | ISSUE_TEMPLATE.md 29 | LICENSE 30 | PULL_REQUEST_TEMPLATE.md 31 | README.md 32 | readme.txt 33 | run-docker*.sh 34 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | DB_NAME=wordpress 2 | DB_HOST=app_db 3 | DB_USER=wordpress 4 | DB_PASSWORD=wordpress 5 | WP_TABLE_PREFIX=wp_ 6 | WP_URL=http://localhost 7 | WP_DOMAIN=localhost 8 | ADMIN_EMAIL=admin@example.com 9 | ADMIN_USERNAME=admin 10 | ADMIN_PASSWORD=root 11 | ADMIN_PATH=/wp-admin 12 | 13 | TEST_DB_NAME=wptests 14 | TEST_DB_HOST=127.0.0.1 15 | TEST_DB_USER=root 16 | TEST_DB_PASSWORD=root 17 | TEST_WP_TABLE_PREFIX=wp_ 18 | 19 | SKIP_DB_CREATE=false 20 | TEST_WP_ROOT_FOLDER=/tmp/wordpress 21 | TEST_ADMIN_EMAIL=admin@wp.test 22 | 23 | TESTS_DIR=tests 24 | TESTS_OUTPUT=tests/_output 25 | TESTS_DATA=tests/_data 26 | TESTS_SUPPORT=tests/_support 27 | TESTS_ENVS=tests/_envs 28 | 29 | CORE_BRANCH=develop 30 | SKIP_TESTS_CLEANUP=1 31 | SUITES=wpunit 32 | 33 | WORDPRESS_DB_HOST=${DB_HOST} 34 | WORDPRESS_DB_USER=${DB_USER} 35 | WORDPRESS_DB_PASSWORD=${DB_PASSWORD} 36 | WORDPRESS_DB_NAME=${DB_NAME} 37 | 38 | # FacetWP is a commercial plugin. Our tests use a private GH repo. 39 | FACET_REPO=github.com/AxeWP/facetwp 40 | # GIT_TOKEN = 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report for WPGraphQL for FacetWP 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: >- 7 | Thank you for taking the time to report a possible bug! 8 | 9 | Please remember, a bug report is not the place to ask questions. You can 10 | use Discord for that, or start a topic in [GitHub 11 | Discussions](https://github.com/AxeWP/wp-graphql-facetwp/discussions). 12 | - type: textarea 13 | attributes: 14 | label: Description 15 | description: >- 16 | Please write a brief description of the bug, including what you expected 17 | and what actually happened. 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Steps to reproduce 23 | description: >- 24 | Please list the all the steps needed to reproduce the bug. Ideally, this 25 | should be in the form of a GraphQL snippet that can be used in 26 | WPGraphiQL IDE. 27 | placeholder: >- 28 | 1. Go to "..." 29 | 2. Query: 30 | ```graphql 31 | query { 32 | 33 | } 34 | 3. Result show X but should be Y 35 | validations: 36 | required: true 37 | - type: textarea 38 | attributes: 39 | label: Additional context 40 | description: >- 41 | Add any other context about the problem here, such as screenshots, error 42 | logs, etc. 43 | - type: input 44 | attributes: 45 | label: Plugin Version 46 | validations: 47 | required: true 48 | - type: input 49 | attributes: 50 | label: FacetWP Version 51 | validations: 52 | required: true 53 | - type: input 54 | attributes: 55 | label: WordPress Version 56 | validations: 57 | required: true 58 | - type: input 59 | attributes: 60 | label: WPGraphQL Version 61 | validations: 62 | required: true 63 | - type: textarea 64 | attributes: 65 | label: Additional enviornmental details 66 | description: PHP version, frontend framework, additional FacetWP extensions, etc. 67 | - type: checkboxes 68 | attributes: 69 | label: Please confirm that you have searched existing issues in the repo. 70 | description: >- 71 | You can do this by searching 72 | https://github.com/AxeWP/wp-graphql-facetwp/issues and making sure the 73 | bug is not related to another plugin. 74 | options: 75 | - label: 'Yes' 76 | required: true 77 | - type: checkboxes 78 | attributes: 79 | label: >- 80 | Please confirm that you have disabled ALL plugins except for FacetWP, WPGraphQL, and WPGraphQL for FacetWP 81 | options: 82 | - label: 'Yes' 83 | required: false 84 | - label: My issue is with a specific 3rd-party plugin. 85 | required: false 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: General Support Request 4 | url: https://github.com/AxeWP/wp-graphql-facetwp/discussions 5 | about: For general help requests, create a new topic in Github Discussions 6 | - name: Premium Support Request 7 | url: https://axepress.dev/contact/ 8 | about: For premium support requests, custom development, or to sponsor a feature, please contact us directly. 9 | - name: Discord Community 10 | url: https://discord.gg/GyKncfmn7q 11 | about: The WPGraphQL Discord is a great place to communicate in real-time. Ask questions, discuss features, get to know other folks using WPGraphQL. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for WPGraphQL for FacetWP 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: >- 7 | Thank you for taking the time to submit a feature request. 8 | 9 | 10 | Please make sure to search the repo for [existing feature 11 | requests](https://github.com/AxeWP/wp-graphql-facetwp/issues) 12 | before creating a new one. 13 | - type: textarea 14 | attributes: 15 | label: What problem does this address? 16 | description: >- 17 | Please describe the problem you are trying to solve, including why you 18 | think this is a problem. 19 | placeholder: I'm always frustrated when [...] 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: What is your proposed solution? 25 | description: >- 26 | Please provide a clear and concise description of your suggested 27 | solution. 28 | placeholder: What I'd like to see happen is [...] 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: What alternatives have you considered? 34 | description: >- 35 | Please list any alternatives you have considered, and why you think your 36 | solution is better. 37 | - type: textarea 38 | attributes: 39 | label: Additional Context 40 | description: Add any other context or screenshots about the feature request here. 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## What 6 | 7 | 8 | ## Why 9 | 10 | 11 | ## How 12 | 13 | 14 | ## Testing Instructions 15 | 16 | 17 | 18 | 19 | 20 | ## Additional Info 21 | 22 | 23 | ## Checklist: 24 | 25 | - [ ] My code is tested to the best of my abilities. 26 | - [ ] My code follows the WordPress Coding Standards. 27 | - [ ] My code has proper inline documentation. 28 | - [ ] I have added unit tests to verify the code works as intended. 29 | - [ ] The changes in this PR have been noted in CHANGELOG.md 30 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request_target: 9 | branches: 10 | - develop 11 | - main 12 | types: [ opened, synchronize, reopened, labeled ] 13 | 14 | 15 | # Cancel previous workflow run groups that have not completed. 16 | concurrency: 17 | # Group workflow runs by workflow name, along with the head branch ref of the pull request 18 | # or otherwise the branch or tag ref. 19 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | run: 24 | runs-on: ubuntu-latest 25 | name: Check code 26 | if: contains(github.event.pull_request.labels.*.name, 'safe to test ✔') || ( github.event_name == 'push' && ( github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) ) 27 | services: 28 | mariadb: 29 | image: mariadb:10 30 | ports: 31 | - 3306:3306 32 | env: 33 | MYSQL_ROOT_PASSWORD: root 34 | # Ensure docker waits for mariadb to start 35 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 36 | 37 | steps: 38 | 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | with: 42 | ref: ${{ github.event.pull_request.head.sha }} 43 | 44 | - name: Setup PHP w/ Composer & WP-CLI 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: 8.2 48 | extensions: mbstring, intl, bcmath, exif, gd, mysqli, opcache, zip, pdo_mysql 49 | coverage: none 50 | tools: composer:v2, wp-cli 51 | 52 | - name: Install dependencies 53 | uses: ramsey/composer-install@v3 54 | with: 55 | composer-options: "--no-progress" 56 | 57 | - name: Setup WordPress 58 | run: | 59 | cp .env.dist .env 60 | echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 61 | composer run install-stan-env 62 | cp -R . /tmp/wordpress/wp-content/plugins/wp-graphql-facetwp 63 | 64 | - name: Run PHPStan 65 | working-directory: /tmp/wordpress/wp-content/plugins/wp-graphql-facetwp 66 | run: composer run-script phpstan 67 | -------------------------------------------------------------------------------- /.github/workflows/code-standard.yml: -------------------------------------------------------------------------------- 1 | name: WordPress Coding Standards 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request: 9 | branches: 10 | - develop 11 | - main 12 | 13 | # Cancel previous workflow run groups that have not completed. 14 | concurrency: 15 | # Group workflow runs by workflow name, along with the head branch ref of the pull request 16 | # or otherwise the branch or tag ref. 17 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | run: 22 | runs-on: ubuntu-latest 23 | name: Checkout repo 24 | steps: 25 | 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup PHP 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: 8.2 33 | extensions: mbstring, intl 34 | tools: composer:v2 35 | coverage: none 36 | 37 | - name: Install dependencies 38 | uses: ramsey/composer-install@v3 39 | with: 40 | composer-options: "--no-progress" 41 | 42 | - name: Run PHP_CodeSniffer 43 | run: composer run-script lint 44 | -------------------------------------------------------------------------------- /.github/workflows/integration-testing.yml: -------------------------------------------------------------------------------- 1 | name: Integration Testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request_target: 9 | branches: 10 | - develop 11 | - main 12 | paths: 13 | - "**.php" 14 | - ".github/workflows/*.yml" 15 | - "!docs/**" 16 | types: [ opened, synchronize, reopened, labeled ] 17 | 18 | # Cancel previous workflow run groups that have not completed. 19 | concurrency: 20 | # Group workflow runs by workflow name, along with the head branch ref of the pull request 21 | # or otherwise the branch or tag ref. 22 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | continuous_integration: 27 | runs-on: ubuntu-latest 28 | name: WordPress ${{ matrix.wordpress }} on PHP ${{ matrix.php }} 29 | if: contains(github.event.pull_request.labels.*.name, 'safe to test ✔') || ( github.event_name == 'push' && ( github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) ) 30 | 31 | strategy: 32 | matrix: 33 | php: ["8.2", "8.1", "8.0"] 34 | wordpress: ["6.7", "6.6", "6.5", "6.4", "6.3", "6.2", "6.1", "6.0"] 35 | include: 36 | - php: "8.2" 37 | wordpress: "6.7" 38 | coverage: 1 39 | # Test old versions against PHP 8.0 40 | - php: "7.4" 41 | wordpress: "6.1" 42 | - php: "7.4" 43 | wordpress: "6.0" 44 | exclude: 45 | # Old WP versions that dont support newer PHP versions 46 | - php: "8.2" 47 | wordpress: "6.0" 48 | # New WP versions that dont support older PHP versions 49 | - php: "8.0" 50 | wordpress: "6.7" 51 | - php: "8.0" 52 | wordpress: "6.6" 53 | - php: "8.0" 54 | wordpress: "6.5" 55 | fail-fast: false 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | ref: ${{ github.event.pull_request.head.sha }} 62 | 63 | - name: Install PHP 64 | uses: shivammathur/setup-php@v2 65 | with: 66 | php-version: ${{ matrix.php }} 67 | extensions: json, mbstring 68 | tools: composer:v2 69 | 70 | - name: Install dependencies 71 | uses: ramsey/composer-install@v3 72 | 73 | - name: Build "testing" Docker Image 74 | env: 75 | PHP_VERSION: ${{ matrix.php }} 76 | WP_VERSION: ${{ matrix.wordpress }} 77 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 78 | run: | 79 | cp .env.dist .env 80 | echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 81 | composer build-test 82 | 83 | - name: Run Acceptance Tests w/ Docker 84 | env: 85 | COVERAGE: ${{ matrix.coverage }} 86 | USING_XDEBUG: ${{ matrix.coverage }} 87 | DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG || matrix.debug }} 88 | SKIP_TESTS_CLEANUP: ${{ matrix.coverage }} 89 | SUITES: acceptance 90 | PHP_VERSION: ${{ matrix.php }} 91 | WP_VERSION: ${{ matrix.wordpress }} 92 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 93 | run: | 94 | cp .env.dist .env 95 | echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 96 | composer run-test 97 | 98 | # - name: Run Unit Tests w/ Docker 99 | # env: 100 | # COVERAGE: ${{ matrix.coverage }} 101 | # USING_XDEBUG: ${{ matrix.coverage }} 102 | # DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG || matrix.debug }} 103 | # SKIP_TESTS_CLEANUP: ${{ matrix.coverage }} 104 | # SUITES: unit 105 | # PHP_VERSION: ${{ matrix.php }} 106 | # WP_VERSION: ${{ matrix.wordpress }} 107 | # GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 108 | # run: | 109 | # cp .env.dist .env 110 | # echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 111 | # composer run-test 112 | 113 | - name: Run WPUnit Tests w/ Docker 114 | env: 115 | COVERAGE: ${{ matrix.coverage }} 116 | USING_XDEBUG: ${{ matrix.coverage }} 117 | DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG || matrix.debug }} 118 | SKIP_TESTS_CLEANUP: ${{ matrix.coverage }} 119 | PHP_VERSION: ${{ matrix.php }} 120 | WP_VERSION: ${{ matrix.wordpress }} 121 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 122 | run: | 123 | cp .env.dist .env 124 | echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 125 | composer run-test 126 | 127 | - name: Push Codecoverage to Coveralls.io 128 | if: ${{ matrix.coverage == 1 }} 129 | env: 130 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | run: vendor/bin/php-coveralls -v 132 | -------------------------------------------------------------------------------- /.github/workflows/schema-linter.yml: -------------------------------------------------------------------------------- 1 | name: Schema Linter 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request_target: 9 | branches: 10 | - develop 11 | - main 12 | types: [ opened, synchronize, reopened, labeled ] 13 | 14 | # Cancel previous workflow run groups that have not completed. 15 | concurrency: 16 | # Group workflow runs by workflow name, along with the head branch ref of the pull request 17 | # or otherwise the branch or tag ref. 18 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 19 | cancel-in-progress: true 20 | 21 | 22 | jobs: 23 | run: 24 | runs-on: ubuntu-latest 25 | name: Lint WPGraphQL Schema 26 | if: contains(github.event.pull_request.labels.*.name, 'safe to test ✔') || ( github.event_name == 'push' && ( github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) ) 27 | 28 | services: 29 | mariadb: 30 | image: mariadb:10 31 | ports: 32 | - 3306:3306 33 | env: 34 | MYSQL_ROOT_PASSWORD: root 35 | # Ensure docker waits for mariadb to start 36 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 37 | 38 | steps: 39 | 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | with: 43 | ref: ${{ github.event.pull_request.head.sha }} 44 | 45 | - name: Setup PHP w/ Composer & WP-CLI 46 | uses: shivammathur/setup-php@v2 47 | with: 48 | php-version: 8.2 49 | extensions: mbstring, intl, bcmath, exif, gd, mysqli, opcache, zip, pdo_mysql 50 | coverage: none 51 | tools: composer:v2, wp-cli 52 | 53 | - name: Setup Node.js 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: 20.x 57 | 58 | - name: Setup GraphQL Schema Linter 59 | run: npm install -g graphql-schema-linter@^3.0 graphql@^16 60 | 61 | - name: Install dependencies 62 | uses: ramsey/composer-install@v3 63 | with: 64 | composer-options: "--no-progress" 65 | 66 | - name: Setup WordPress 67 | run: | 68 | cp .env.dist .env 69 | echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 70 | composer run install-test-env 71 | 72 | - name: Register Facet to WPGraphQL 73 | run: | 74 | mkdir -p /tmp/wordpress/wp-content/mu-plugins/ 75 | echo " /tmp/wordpress/wp-content/mu-plugins/test-facet.php 76 | 77 | - name: Generate the Static Schema 78 | run: | 79 | cd /tmp/wordpress/ 80 | # Output: /tmp/schema.graphql 81 | wp graphql generate-static-schema 82 | 83 | - name: Lint the Static Schema 84 | run: | 85 | graphql-schema-linter --except=relay-connection-types-spec,relay-page-info-spec --ignore '{"defined-types-are-used":["MenuItemsWhereArgs","PostObjectUnion","TermObjectUnion","TimezoneEnum"], "fields-have-descriptions":["WPGatsbyCompatibility","WPGatsbyPageNode","WPGatsbyPreviewStatus"], "enum-values-have-descriptions":["WPGatsbyRemotePreviewStatusEnum","WPGatsbyWPPreviewedNodeStatus"], "fields-are-camel-cased":["FacetPager"], "input-object-values-are-camel-cased":["PostFacetPager"]}' /tmp/schema.graphql 86 | 87 | - name: Display ignored linting errors 88 | run: | 89 | graphql-schema-linter /tmp/schema.graphql || true 90 | 91 | - name: Get Latest tag 92 | uses: actions-ecosystem/action-get-latest-tag@v1 93 | id: get-latest-tag 94 | 95 | - name: Test Schema for breaking changes 96 | run: | 97 | echo "Previous tagged schema ${{ steps.get-latest-tag.outputs.tag }}" 98 | 99 | - name: Get Previous Released Schema 100 | run: curl 'https://github.com/AxeWP/wp-graphql-facetwp/releases/download/${{ steps.get-latest-tag.outputs.tag }}/schema.graphql' -L --output /tmp/${{ steps.get-latest-tag.outputs.tag }}.graphql 101 | 102 | # https://github.com/marketplace/actions/graphql-inspector 103 | - name: Install Schema Inspector 104 | run: | 105 | npm install @graphql-inspector/config @graphql-inspector/cli graphql 106 | 107 | - name: Run Schema Inspector 108 | run: | 109 | # This schema and previous release schema 110 | node_modules/.bin/graphql-inspector diff /tmp/${{ steps.get-latest-tag.outputs.tag }}.graphql /tmp/schema.graphql 111 | -------------------------------------------------------------------------------- /.github/workflows/upload-release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Release Package 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | tag: 9 | name: Upload New Release 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: 7.4 20 | extensions: mbstring, intl 21 | tools: composer 22 | 23 | - name: Install dependencies 24 | uses: ramsey/composer-install@v3 25 | with: 26 | composer-options: "--no-progress --no-dev --optimize-autoloader" 27 | 28 | - name: Build and zip 29 | run: | 30 | composer run-script zip 31 | 32 | - name: Upload artifact 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: wp-graphql-facetwp 36 | path: plugin-build/wp-graphql-facetwp.zip 37 | 38 | - name: Upload release asset 39 | uses: softprops/action-gh-release@v2 40 | with: 41 | files: plugin-build/wp-graphql-facetwp.zip 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/upload-schema-artifact.yml: -------------------------------------------------------------------------------- 1 | name: Upload Schema Artifact 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | name: Generate and Upload WPGraphQL Schema Artifact 11 | 12 | services: 13 | mariadb: 14 | image: mariadb:10 15 | ports: 16 | - 3306:3306 17 | env: 18 | MYSQL_ROOT_PASSWORD: root 19 | # Ensure docker waits for mariadb to start 20 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup PHP w/ Composer & WP-CLI 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: 8.2 30 | extensions: mbstring, intl, bcmath, exif, gd, mysqli, opcache, zip, pdo_mysql 31 | coverage: none 32 | tools: composer:v2, wp-cli 33 | 34 | - name: Setup WordPress 35 | run: | 36 | cp .env.dist .env 37 | echo GIT_TOKEN=${{ secrets.GIT_TOKEN }} >> .env 38 | composer run install-test-env 39 | 40 | - name: Generate the Static Schema 41 | run: | 42 | cd /tmp/wordpress/ 43 | # Output: /tmp/schema.graphql 44 | wp graphql generate-static-schema 45 | 46 | - name: Upload schema as release artifact 47 | uses: softprops/action-gh-release@v2 48 | with: 49 | files: /tmp/schema.graphql 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hidden files 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # IDE Files 6 | .devcontainer/* 7 | .devcontainer.json 8 | .vscode 9 | .idea 10 | 11 | # Environment variables for testing 12 | .env 13 | .env.* 14 | !.env.dist 15 | 16 | # Ruleset Overrides 17 | phpcs.xml 18 | phpunit.xml 19 | phpstan.neon 20 | 21 | # Directory to generate the dist zipfile 22 | plugin-build 23 | 24 | # Composer auth 25 | auth.json 26 | 27 | # Composer deps 28 | vendor 29 | vendor-prefixed 30 | 31 | # NPM deps 32 | node_modules 33 | 34 | # Generated Schema used in some tooling. Versioned Schema is uploaded as a Release artifact to Github. 35 | schema.graphql 36 | 37 | # WP CLI config overrides 38 | wp-cli.local.yml 39 | 40 | # Tests 41 | *.sql 42 | *.tar.gz 43 | !tests 44 | tests/*.suite.yml 45 | coverage/* 46 | build/ 47 | .log/ 48 | 49 | # Strauss installer 50 | bin/strauss.phar 51 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sniffs for the WPGraphQL plugin ecosystem 4 | 5 | 6 | ./access-functions.php 7 | ./wp-graphql-facetwp.php 8 | ./src/ 9 | /vendor/ 10 | /node_modules/ 11 | */tests/* 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## v0.5.1 6 | 7 | This _minor_ release bumps the `tested up to` tags for WordPress v6.7.2 and WPGraphQL v2.0. 8 | 9 | It's also the first release in the new repository location at https://github.com/AxeWP/wp-graphql-facetwp, a change which should allow @justlevine to allocate significantly more resources to the plugin. Thanks @hsimah for your stewardship and hospitality 🙏. 10 | 11 | 12 | 13 | > [!IMPORTANT] 14 | > Vendor files are now .gitignored and must be built locally (`composer install`) for the plugin source code to work. 15 | > 16 | > Users should download the release version of wp-graphql-facetwp.zip provided on the [latest GitHub Release page](https://github.com/AxeWP/wp-graphql-facetwp/releases/latest) and _not_ the source code zip file. 17 | 18 | - chore!: Remove `vendor` and `vendor-prefixed/*` from the GitHub repository. 19 | - chore: Test compatibility with WordPress 6.7.2. 20 | - chore: Test compatibility with WPGraphQL 2.0. 21 | - chore: Update Strauss to v0.19.1. 22 | - chore: Update composer dependencies. 23 | - chore: Update repository URLs to `https://github.com/AxeWP/wp-graphql-facetwp`. H/t @hsimah 24 | - ci: Cleanup zip and release workflow. 25 | - ci: Use `docker compose` instead of `docker-compose`. 26 | 27 | ## v0.5.0 28 | 29 | This _major_ release refactors the root files to use the `WPGraphQL\FacetWP` namespace. It also adds support for the Plugin Dependencies header added in WordPress 6.5, adds explicit support for PHP 8.2 and WordPress 6.5, and more. 30 | 31 | > [!NOTE] 32 | > Although this release technically contains breaking changes, these changes are limited to developers directly extending the `wp-graphql-facetwp.php` file and `WPGraphQL\FacetWP\Main` class. 33 | > If you are using the plugin as intended, you should not experience any issues when upgrading. 34 | 35 | - feat: Add support for Plugin Dependencies header. 36 | - chore!: Refactor plugin entrypoint to use `WPGraphQL\FacetWP` namespace. 37 | - chore: Implement strict phpstan rules and lint. 38 | - chore: Update Composer dependencies and lint. 39 | - chore: Update WPGraphQL Plugin Boilerplate to v0.1.0. 40 | - ci: Test against WP 6.5. 41 | - ci: Test against PHP 8.2. 42 | - ci: Update GitHub Workflows to latest versions. 43 | - ci: Update Strauss to v0.17.0. 44 | 45 | ## v0.4.4 46 | 47 | This _minor_ release implements the new WPGraphQL Coding Standards ruleset for `PHP_CodeSniffer`. While many of the addressed sniffs are cosmetic, numerous smells regarding performance, type safety, sanitization, and 3rd-party interoperability have been fixed as well. 48 | 49 | - chore: Implement `axepress/wp-graphql-cs` PHP_Codesniffer ruleset. 50 | - chore: Update WPGraphQL Plugin Boilerplate to v0.0.9. 51 | - chore: Update Composer dev-dependencies. 52 | 53 | ## v0.4.3 54 | 55 | This _minor_ release adds support for the [Sort Facet](https://facetwp.com/help-center/facets/facet-types/sort). It also fixes a bug where the FacetQueryArgs input value was not being correctly matched to the correct Facet. 56 | 57 | **Note:** To support the Sort facet when using custom WPGraphQL Connection Resolvers, you must set the connection's `orderby` argument to `post__id`. The [example WooCommerce snippet](./README.md#woocommerce-support) has been updated to reflect this change, and you should update your custom code accordingly. 58 | 59 | - feat: Add support for the Sort Facet. (Props to @ninie1205 for sponsoring this feature!) 60 | - fix: Fallback to `snake_case` when matching the `FacetQueryArgs` input value to the FacetWP facet name. 61 | - docs: Update WooCommerce snippet in README.md to support the Sort Facet. 62 | 63 | ## v0.4.2 64 | 65 | This _minor_ release lays the groundwork for the upcoming Facet auto-registration / official Sort Facet support. It introduces a new `FacetConfig` interface, which is implemented by the `Facet` object. Additionally, we adopted the use of WPGraphQL Plugin Boilerplate to scaffold our PHP classes, updated our Composer dev dependencies, and started testing against WordPress 6.2 and running WPUnit tests as part of our CI workflow. 66 | 67 | - feat: Change `Facet` object to implement new `FacetConfig` interface. 68 | - fix: Add missing descriptions to GraphQL types. 69 | - dev!: Refactor GraphQL type classes to use `axepress/wp-graphql-plugin-boilerplate`. 70 | - dev!: Remove unused PHP interfaces. 71 | - dev: Initialize plugin using `facetwp_init` hook. 72 | - chore: Build `FacetQueryArgs` config before calling the registration method. 73 | - chore: Stub `FacetWP_Facet` class properties. 74 | - chore: Update Composer dev deps. 75 | - chore: Implement Strauss to namespace PHP dependencies. 76 | - chore: Fix doc reference and internal usage of `register_graphql_facet_type()` function to be called on `graphql_facetwp_init` hook. 77 | - tests: Refactor and enable WPUnit tests. 78 | - ci: Test against WordPress 6.2. 79 | - ci: Register test post facet so `graphql-schema-linter` generates a valid schema. 80 | - ci: Run GitHub workflows on `push` events to `main` or `develop` branches. 81 | - ci: Temporary ignore `graphql-schema-linter` errors from soon-to-be deprecated Types. 82 | 83 | ## v0.4.1 84 | This _minor_ release introduces WPGraphQL-specific field properties to the Facet configuration array and adds the corresponding `get_graphql_allowed_facets()` access function. It also deprecates the usage `snake_case` autogenerated field names in the `FacetQueryArgs` input type in favor of `camelCase`, and adds explicit support for PHP 8.1. 85 | 86 | - feat: add `show_in_graphql` and `graphql_field_name` to the Facet configuration. 87 | - feat: add explicit PHP 8.1 support. 88 | - feat: deprecate usage of `snake_case` field names in `FacetQueryArgs` input type, in favor of `camelCase`. 89 | - dev: add `get_graphql_allowed_facets()` access function. 90 | - dev: refactor facet input types to use the `graphql_type` config property generated by `FacetRegistry::get_facet_input_type()`. 91 | - dev: add the following WordPress filters: `graphql_facetwp_facet_input_type`. 92 | - chore: update Composer dependencies. 93 | - chore: replace `poolshark/wp-graphql-stubs` dev dependency with `axepress/wp-graphql-stubs` 94 | - chore: stub `FWP()` function and `FacetWP` class properties. 95 | - chore: change stubfile extensions to `.php`. 96 | - tests: change `FWPGraphQLTestCase.php::register_facet()` to add a new facet instead of replace it. 97 | 98 | ## v0.4.0 99 | This _major_ release refactors the underlying PHP codebase, bringing with it support for the latest versions of WPGraphQL and FacetWP. Care has been taken to ensure there are _no breaking changes_ to the GraphQL schema. 100 | 101 | - feat!: Refactor plugin PHP classes and codebase structure to follow ecosystem patterns. 102 | - feat!: Bump minimum version of WPGraphQL to `v1.6.1`. 103 | - feat!: Bump minimum PHP version to `v7.4`. 104 | - feat!: Bump minimum FacetWP version to `v4.0`. 105 | - fix: Implement `WPVIP` PHP coding standards. 106 | - fix: Implement and meet `PHPStan` level 8 coding standards. 107 | - tests: Implement basic Codeception acceptance tests. 108 | - ci: Add Github workflows for PRs and releases. 109 | - chore: update Composer dependencies. 110 | - chore: switch commit flow to `develop` => `main` and set default branch to `develop`. The existing `master` branch will be removed on 1 October 2022. 111 | 112 | ## v0.3.0 113 | - feat: Updates with default connection inputs and WooCommerce integration hooks. 114 | 115 | ## v0.2.0 116 | - fix: Updated deprecated calls to WPGraphQL functions. 117 | - docs: Updated documentation to remind users to register Facets during GraphQL init. 118 | 119 | ## v0.1.1 120 | - fix: facet connection used old style node resolver. 121 | 122 | ## v0.1.0 123 | Initial release. 124 | 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](./logo.png) 2 | # WPGraphQL for FacetWP 3 | 4 | Adds WPGraphQL support for [FacetWP](https://facetwp.com/). 5 | 6 | * [Join the WPGraphQL community on Discord.](https://discord.gg/GyKncfmn7q) 7 | * [Documentation](#usage) 8 | ----- 9 | 10 | ![Packagist License](https://img.shields.io/packagist/l/hsimah-services/wp-graphql-facetwp?color=green) ![Packagist Version](https://img.shields.io/packagist/v/hsimah-services/wp-graphql-facetwp?label=stable) ![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/AxeWP/wp-graphql-facetwp/0.5.1) ![GitHub forks](https://img.shields.io/github/forks/AxeWP/wp-graphql-facetwp?style=social) ![GitHub Repo stars](https://img.shields.io/github/stars/AxeWP/wp-graphql-facetwp?style=social)
11 | ![CodeQuality](https://img.shields.io/github/actions/workflow/status/AxeWP/wp-graphql-facetwp/code-quality.yml?branch=develop&label=Code%20Quality) 12 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/AxeWP/wp-graphql-facetwp/integration-testing.yml?branch=develop&label=Integration%20Testing) 13 | ![Coding Standards](https://img.shields.io/github/actions/workflow/status/AxeWP/wp-graphql-facetwp/code-standard.yml?branch=develop&label=WordPress%20Coding%20Standards) 14 | 15 | ----- 16 | ## Overview 17 | 18 | This plugin exposes configured facets through the graph schema. Once registered for a type, a query is available. The payload includes both facet choices and information and a connection to the post type data. This allows for standard GraphQL pagination of the returned data set. 19 | 20 | This plugin has been tested and is functional with SearchWP. 21 | 22 | ## System Requirements 23 | 24 | * PHP 7.4-8.1.x 25 | * WordPress 5.4.1+ 26 | * WPGraphQL 1.6.0+ (1.9.0+ recommended) 27 | * FacetWP 4.0+ 28 | 29 | ## Quick Install 30 | 31 | 1. Install & activate [WPGraphQL](https://www.wpgraphql.com/). 32 | 2. Install & activate [FacetWP](https://facetwp.com/). 33 | 3. Download the `wp-graphql-facetwp.zip` file from the [latest release](https://github.com/AxeWP/wp-graphql-facetwp/releases/latest) upload it to your WordPress install, and activate the plugin. 34 | 35 | > [!IMPORTANT] 36 | > 37 | > Make sure you are downloading the [`wp-graphql-facetwp.zip`](https://github.com/AxeWP/wp-graphql-facetwp/releases/latest/download/wp-graphql-facetwp.zip) file from the releases page, not the `Source code (zip)` file nor a clone of the repository. 38 | > 39 | > If you wish to use the source code, you will need to run `composer install` inside the plugin folder to install the required dependencies. 40 | 41 | ### With Composer 42 | 43 | ```console 44 | composer require hsimah-services/wp-graphql-facetwp 45 | ``` 46 | ## Updating and Versioning 47 | 48 | As we work towards a 1.0 Release, we will need to introduce **numerous** breaking changes. We will do our best to group multiple breaking changes together in a single release, to make it easier on developers to keep their projects up-to-date. 49 | 50 | Until we hit v1.0, we're using a modified version of [SemVer](https://semver.org/), where: 51 | 52 | * v0.**x**: "Major" releases. These releases introduce new features, and _may_ contain breaking changes to either the PHP API or the GraphQL schema 53 | * v0.x.**y**: "Minor" releases. These releases introduce new features and enhancements and address bugs. They _do not_ contain breaking changes. 54 | * v0.x.y.**z**: "Patch" releases. These releases are reserved for addressing issue with the previous release only. 55 | 56 | ## Development and Support 57 | 58 | WPGraphQL for FacetWP was initially created by [Hamish Blake](https://www.hsimah.com/). Maintenance and development are now provided by [AxePress Development](https://axepress.dev/). On 15 February 2025, the repository was transferred from https://github.com/hsimah-services to https://github.com/AxeWP/wp-graphql-facetwp. 59 | 60 | Community contributions are _welcome_ and **encouraged**. 61 | 62 | Basic support is provided for free, both in [this repo](https://github.com/AxeWP/wp-graphql-facetwp/issues) and at the `#facetwp` channel in [WPGraphQL Discord](https://discord.gg/GyKncfmn7q). 63 | 64 | Priority support and custom development is available to [AxePress Development sponsors](https://github.com/sponsors/AxeWP). 65 | 66 | ## Usage: 67 | 68 | - _The WPGraphQL documentation can be found [here](https://docs.wpgraphql.com)._
69 | - _The FacetWP documentation can be found [here](https://facetwp.com/documentation/)._ 70 | 71 | ### Registering a facet to WPGraphQL 72 | 73 | **It is assumed that facets have been configured.** 74 | 75 | To register a FacetWP query in the WPGraphQL schema for a WordPress post type (eg `post`) simply call the following function: 76 | 77 | ```php 78 | // Register facet for Posts 79 | add_action( 'graphql_facetwp_init', function () { 80 | register_graphql_facet_type( 'post' ); 81 | } ); 82 | ``` 83 | 84 | This will create a WPGraphQL `postFacet` field on the `RootQuery`. The payload includes a collection of queried `facets` and a `posts` connection. The connection is a standard WPGraphQL connection supporting pagination and server side ordering. The connection payload only includes filtered posts. 85 | 86 | ### Example query 87 | 88 | 89 | **Note** This is not a complete list of GraphQL fields and types added to the schema. Please refer to the WPGraphiQL IDE for more queries and their documentation. 90 | 91 | ```graphql 92 | query GetPostsByFacet( $query: FacetQueryArgs, $after: String, $search: String, $orderBy: [PostObjectsConnectionOrderbyInput] ) { 93 | postFacet( 94 | where: { 95 | status: PUBLISH, 96 | query: $query # The query arguments are determined by the Facet type. 97 | } 98 | ) { 99 | facets { # The facet configuration 100 | selected 101 | name 102 | label 103 | choices { 104 | value 105 | label 106 | count 107 | } 108 | } 109 | posts ( # The results of the facet query. Can be filtered by WPGraphQL connection where args 110 | first: 10, 111 | after: $after, 112 | where: { search: $search, orderby: $orderBy} # The `orderby` arg is ignored if using the Sort facet. 113 | ) { 114 | pageInfo { 115 | hasNextPage 116 | endCursor 117 | } 118 | nodes { 119 | title 120 | excerpt 121 | } 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | ### WooCommerce Support 128 | 129 | Support for WooCommerce Products can be added with following configuration: 130 | 131 | ```php 132 | // This is the same as all CPTs. 133 | add_action( 'graphql_facetwp_init', function () { 134 | register_graphql_facet_type( 'product' ); 135 | }); 136 | 137 | // This is required because WooGQL uses a custom connection resolver. 138 | add_filter( 'facetwp_graphql_facet_connection_config', 139 | function ( array $default_graphql_config, array $facet_config ) { 140 | $type = $config['type']; 141 | 142 | $use_graphql_pagination = \WPGraphQL\FacetWP\Registry\FacetRegistry::use_graphql_pagination(); 143 | 144 | return array_merge( 145 | $default_graphql_config, 146 | [ 147 | 'connectionArgs' => \WPGraphQL\WooCommerce\Connection\Products::get_connection_args(), 148 | 'resolveNode' => function ( $node, $_args, $context ) use ( $type ) { 149 | return $context->get_loader( $type )->load_deferred( $node->ID ); 150 | }, 151 | 'resolve' => function ( $source, $args, $context, $info ) use ( $type, $use_graphql_pagination ) { 152 | // If we're using FWP's offset pagination, we need to override the connection args. 153 | if ( ! $use_graphql_pagination ) { 154 | $args['first'] = $source['pager']['per_page']; 155 | } 156 | 157 | $resolver = new \WPGraphQL\Data\Connection\PostObjectConnectionResolver( $source, $args, $context, $info, $type ); 158 | 159 | // Override the connection results with the FWP results. 160 | if( ! empty( $source['results'] ) ) { 161 | $resolver->->set_query_arg( 'post__in', $source['results'] ); 162 | } 163 | 164 | // Use post__in when delegating sorting to FWP. 165 | if ( ! empty( $source['is_sort'] ) ) { 166 | $resolver->set_query_arg( 'orderby', 'post__in' ); 167 | } elseif( 'product' === $type ) { 168 | // If we're relying on WPGQL to sort, we need to to handle WooCommerce meta. 169 | $resolver = Products::set_ordering_query_args( $resolver, $args ); 170 | } 171 | 172 | return $resolver ->get_connection(); 173 | }, 174 | ] 175 | ); 176 | }, 177 | 100, 178 | 2 179 | ); 180 | ``` 181 | 182 | ### Limitations 183 | Currently the plugin only has been tested using Checkbox, Radio, and Sort facet types. Support for additional types is in development. 184 | 185 | ## Testing 186 | 187 | 1. Update your `.env` file to your testing environment specifications. 188 | 2. Run `composer install-test-env` to create the test environment. 189 | 3. Run your test suite with [Codeception](https://codeception.com/docs/02-GettingStarted#Running-Tests). 190 | E.g. `vendor/bin/codecept run wpunit` will run all WPUnit tests. 191 | -------------------------------------------------------------------------------- /access-functions.php: -------------------------------------------------------------------------------- 1 | 42 | * 43 | * @since 0.4.1 44 | */ 45 | function get_graphql_allowed_facets(): array { 46 | return FacetRegistry::get_allowed_facets(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bin/_env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set +u 4 | 5 | print_usage_instruction() { 6 | echo "ERROR!" 7 | echo "Values in the .env file are missing or incorrect." 8 | echo "Open the .env file at the root of this plugin and enter values to match your local environment settings" 9 | exit 1 10 | } 11 | 12 | if [[ -z "$TEST_DB_NAME" ]]; then 13 | echo "TEST_DB_NAME not found" 14 | print_usage_instruction 15 | else 16 | DB_NAME=$TEST_DB_NAME 17 | fi 18 | if [[ -z "$TEST_DB_USER" ]]; then 19 | echo "TEST_DB_USER not found" 20 | print_usage_instruction 21 | else 22 | DB_USER=$TEST_DB_USER 23 | fi 24 | 25 | DB_HOST=${TEST_DB_HOST-localhost} 26 | DB_PASS=${TEST_DB_PASSWORD-""} 27 | WP_VERSION=${WP_VERSION-latest} 28 | TMPDIR=${TMPDIR-/tmp} 29 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 30 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 31 | WP_CORE_DIR=${TEST_WP_ROOT_FOLDER-$TMPDIR/wordpress/} 32 | PLUGIN_DIR=$(pwd) 33 | if [[ ! -z "$PLUGIN_SLUG" ]]; then 34 | PLUGIN_DIR="${PLUGIN_DIR}/${PLUGIN_SLUG}" 35 | echo "Using $PLUGIN_DIR as source" 36 | fi 37 | 38 | WP_CLI_CONFIG_PATH="${PLUGIN_DIR}/bin/wp-cli.yml" 39 | 40 | SKIP_DB_CREATE=${SKIP_DB_CREATE-false} 41 | -------------------------------------------------------------------------------- /bin/_lib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set +u 4 | 5 | download() { 6 | if [ $(which curl) ]; then 7 | curl -s "$1" >"$2" 8 | elif [ $(which wget) ]; then 9 | wget -nv -O "$2" "$1" 10 | fi 11 | } 12 | 13 | install_wordpress() { 14 | 15 | if [ -d $WP_CORE_DIR ]; then 16 | return 17 | fi 18 | 19 | mkdir -p $WP_CORE_DIR 20 | 21 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 22 | mkdir -p $TMPDIR/wordpress-nightly 23 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 24 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 25 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 26 | else 27 | if [ $WP_VERSION == 'latest' ]; then 28 | local ARCHIVE_NAME='latest' 29 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 30 | # https serves multiple offers, whereas http serves single. 31 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 32 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 33 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 34 | LATEST_VERSION=${WP_VERSION%??} 35 | else 36 | # otherwise, scan the releases and get the most up to date minor version of the major release 37 | local VERSION_ESCAPED=$(echo $WP_VERSION | sed 's/\./\\\\./g') 38 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 39 | fi 40 | if [[ -z "$LATEST_VERSION" ]]; then 41 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 42 | else 43 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 44 | fi 45 | else 46 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 47 | fi 48 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 49 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 50 | fi 51 | 52 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 53 | } 54 | 55 | install_db() { 56 | if [ ${SKIP_DB_CREATE} = "true" ]; then 57 | return 0 58 | fi 59 | 60 | # parse DB_HOST for port or socket references 61 | local PARTS=(${DB_HOST//\:/ }) 62 | local DB_HOSTNAME=${PARTS[0]} 63 | local DB_SOCK_OR_PORT=${PARTS[1]} 64 | local EXTRA="" 65 | 66 | if ! [ -z $DB_HOSTNAME ]; then 67 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 68 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 69 | elif ! [ -z $DB_SOCK_OR_PORT ]; then 70 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 71 | elif ! [ -z $DB_HOSTNAME ]; then 72 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 73 | fi 74 | fi 75 | 76 | # create database 77 | RESULT=$(mysql -u $DB_USER --password="$DB_PASS" --skip-column-names -e "SHOW DATABASES LIKE '$DB_NAME'"$EXTRA) 78 | if [ "$RESULT" != $DB_NAME ]; then 79 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 80 | fi 81 | } 82 | 83 | configure_wordpress() { 84 | if [ "${SKIP_WP_SETUP}" = "true" ]; then 85 | echo "Skipping WordPress setup..." 86 | return 0 87 | fi 88 | 89 | cd $WP_CORE_DIR 90 | 91 | echo "Setting up WordPress..." 92 | export WP_CLI_CONFIG_PATH=${WP_CLI_CONFIG_PATH} 93 | 94 | wp config create --dbname="$DB_NAME" --dbuser="$DB_USER" --dbpass="$DB_PASS" --dbhost="$DB_HOST" --skip-check --force=true 95 | wp core install --url=$WP_DOMAIN --title=Test --admin_user=$ADMIN_USERNAME --admin_password=$ADMIN_PASSWORD --admin_email=$ADMIN_EMAIL 96 | wp rewrite structure '/%year%/%monthnum%/%postname%/' --hard 97 | } 98 | 99 | install_facetwp() { 100 | if [ ! -d $WP_CORE_DIR/wp-content/plugins/facetwp ]; then 101 | echo "Cloning FacetWP: https://${GIT_TOKEN}@${FACET_REPO}" 102 | git clone -b master --single-branch https://${GIT_TOKEN}@${FACET_REPO}.git $WP_CORE_DIR/wp-content/plugins/facetwp 103 | fi 104 | echo "Activating FacetWP" 105 | wp plugin activate facetwp --allow-root 106 | } 107 | 108 | install_plugins() { 109 | cd $WP_CORE_DIR 110 | 111 | install_facetwp 112 | 113 | # Install WPGraphQL and Activate 114 | if ! $(wp plugin is-installed wp-graphql --allow-root); then 115 | wp plugin install wp-graphql --allow-root 116 | fi 117 | wp plugin activate wp-graphql --allow-root 118 | } 119 | 120 | setup_plugin() { 121 | if [ "${SKIP_WP_SETUP}" = "true" ]; then 122 | echo "Skipping WPGraphQL for FacetWP installation..." 123 | return 0 124 | fi 125 | 126 | # Add this repo as a plugin to the repo 127 | if [ ! -d $WP_CORE_DIR/wp-content/plugins/wp-graphql-facetwp ]; then 128 | ln -s $PLUGIN_DIR $WP_CORE_DIR/wp-content/plugins/wp-graphql-facetwp 129 | cd $WP_CORE_DIR/wp-content/plugins 130 | pwd 131 | ls 132 | fi 133 | 134 | cd $PLUGIN_DIR 135 | 136 | composer install 137 | } 138 | 139 | post_setup() { 140 | cd $WP_CORE_DIR 141 | 142 | # activate the plugin 143 | wp plugin activate wp-graphql-facetwp --allow-root 144 | 145 | # Flush the permalinks 146 | wp rewrite flush --allow-root 147 | 148 | # Export the db for codeception to use 149 | wp db export $PLUGIN_DIR/tests/_data/dump.sql --allow-root 150 | 151 | echo "Installed plugins" 152 | wp plugin list --allow-root 153 | } 154 | -------------------------------------------------------------------------------- /bin/install-stan-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! -f ".env" ]]; then 4 | echo "No .env file was detected. .env.dist has been copied to .env" 5 | echo "Open the .env file and enter values to match your local environment" 6 | cp .env.dist .env 7 | fi 8 | 9 | source .env 10 | 11 | BASEDIR=$(dirname "$0"); 12 | source ${BASEDIR}/_env.sh 13 | source ${BASEDIR}/_lib.sh 14 | 15 | install_wordpress 16 | install_db 17 | configure_wordpress 18 | install_plugins 19 | post_setup 20 | -------------------------------------------------------------------------------- /bin/install-test-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! -f ".env" ]]; then 4 | echo "No .env file was detected. .env.dist has been copied to .env" 5 | echo "Open the .env file and enter values to match your local environment" 6 | cp .env.dist .env 7 | fi 8 | 9 | source .env 10 | 11 | BASEDIR=$(dirname "$0"); 12 | source ${BASEDIR}/_env.sh 13 | source ${BASEDIR}/_lib.sh 14 | 15 | install_wordpress 16 | install_db 17 | configure_wordpress 18 | install_plugins 19 | setup_plugin 20 | post_setup 21 | -------------------------------------------------------------------------------- /bin/run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | ## 6 | # Use this script through Composer scripts in the package.json. 7 | # To quickly build and run the docker-compose scripts for an app or automated testing 8 | # run the command below after run `composer install --no-dev` with the respectively 9 | # flag for what you need. 10 | ## 11 | print_usage_instructions() { 12 | echo "Usage: composer build-and-run -- [-a|-t]" 13 | echo " -a Spin up a WordPress installation." 14 | echo " -t Run the automated tests." 15 | echo "Example use:" 16 | echo " composer build-app" 17 | echo " composer run-app" 18 | echo "" 19 | echo " WP_VERSION=6.7 PHP_VERSION=8.2 composer build-app" 20 | echo " WP_VERSION=6.7 PHP_VERSION=8.2 composer run-app" 21 | echo "" 22 | echo " WP_VERSION=6.7 PHP_VERSION=8.2 bin/run-docker.sh build -a" 23 | echo " WP_VERSION=6.7 PHP_VERSION=8.2 bin/run-docker.sh run -a" 24 | exit 1 25 | } 26 | 27 | if [ $# -eq 0 ]; then 28 | print_usage_instructions 29 | fi 30 | 31 | TAG=${TAG-latest} 32 | WP_VERSION=${WP_VERSION-6.7} 33 | PHP_VERSION=${PHP_VERSION-8.2} 34 | 35 | BUILD_NO_CACHE=${BUILD_NO_CACHE-} 36 | 37 | if [[ ! -f ".env" ]]; then 38 | echo "No .env file was detected. .env.dist has been copied to .env" 39 | echo "Open the .env file and enter values to match your local environment" 40 | cp .env.dist .env 41 | fi 42 | 43 | subcommand=$1 44 | shift 45 | case "$subcommand" in 46 | "build") 47 | while getopts ":cat" opt; do 48 | case ${opt} in 49 | c) 50 | echo "Build without cache" 51 | BUILD_NO_CACHE=--no-cache 52 | ;; 53 | a) 54 | docker build $BUILD_NO_CACHE -f docker/app.Dockerfile \ 55 | -t wp-graphql-facetwp:${TAG}-wp${WP_VERSION}-php${PHP_VERSION} \ 56 | --build-arg WP_VERSION=${WP_VERSION} \ 57 | --build-arg PHP_VERSION=${PHP_VERSION} \ 58 | . 59 | ;; 60 | t) 61 | docker build $BUILD_NO_CACHE -f docker/app.Dockerfile \ 62 | -t wp-graphql-facetwp:${TAG}-wp${WP_VERSION}-php${PHP_VERSION} \ 63 | --build-arg WP_VERSION=${WP_VERSION} \ 64 | --build-arg PHP_VERSION=${PHP_VERSION} \ 65 | . 66 | 67 | docker build $BUILD_NO_CACHE -f docker/testing.Dockerfile \ 68 | -t wp-graphql-facetwp-testing:${TAG}-wp${WP_VERSION}-php${PHP_VERSION} \ 69 | --build-arg WP_VERSION=${WP_VERSION} \ 70 | --build-arg PHP_VERSION=${PHP_VERSION} \ 71 | . 72 | ;; 73 | \?) print_usage_instructions ;; 74 | *) print_usage_instructions ;; 75 | esac 76 | done 77 | shift $((OPTIND - 1)) 78 | ;; 79 | "run") 80 | while getopts "e:at" opt; do 81 | case ${opt} in 82 | a) 83 | WP_VERSION=${WP_VERSION} PHP_VERSION=${PHP_VERSION} docker compose up app 84 | ;; 85 | t) 86 | export APACHE_RUN_USER=$(id -u) 87 | export APACHE_RUN_GROUP=$(id -g) 88 | echo "APACHE RUN USER ${APACHE_RUN_USER} ${APACHE_RUN_GROUP}" 89 | 90 | docker compose run --rm \ 91 | -e COVERAGE=${COVERAGE-} \ 92 | -e USING_XDEBUG=${USING_XDEBUG-} \ 93 | -e DEBUG=${DEBUG-} \ 94 | -e WP_VERSION=${WP_VERSION} \ 95 | -e PHP_VERSION=${PHP_VERSION} \ 96 | testing --scale app=0 97 | ;; 98 | \?) print_usage_instructions ;; 99 | *) print_usage_instructions ;; 100 | esac 101 | done 102 | shift $((OPTIND - 1)) 103 | ;; 104 | 105 | \?) print_usage_instructions ;; 106 | *) print_usage_instructions ;; 107 | esac 108 | -------------------------------------------------------------------------------- /bin/wp-cli.yml: -------------------------------------------------------------------------------- 1 | apache_modules: 2 | - mod_rewrite 3 | -------------------------------------------------------------------------------- /codeception.dist.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: '%TESTS_DIR%' 3 | output: '%TESTS_OUTPUT%' 4 | data: '%TESTS_DATA%' 5 | support: '%TESTS_SUPPORT%' 6 | envs: '%TESTS_ENVS%' 7 | params: 8 | - env 9 | - .env 10 | actor_suffix: Tester 11 | bootstrap: bootstrap.php 12 | settings: 13 | colors: true 14 | memory_limit: 1024M 15 | coverage: 16 | enabled: true 17 | remote: false 18 | c3_url: '%WP_URL%/wp-content/plugins/wp-graphql-facetwp/wp-graphql-facetwp.php' 19 | include: 20 | - src/* 21 | exclude: 22 | - wp-graphql-facetwp.php 23 | - vendor/* 24 | show_only_summary: false 25 | extensions: 26 | enabled: 27 | - Codeception\Extension\RunFailed 28 | commands: 29 | - Codeception\Command\GenerateWPUnit 30 | - Codeception\Command\GenerateWPRestApi 31 | - Codeception\Command\GenerateWPRestController 32 | - Codeception\Command\GenerateWPRestPostTypeController 33 | - Codeception\Command\GenerateWPAjax 34 | - Codeception\Command\GenerateWPCanonical 35 | - Codeception\Command\GenerateWPXMLRPC 36 | modules: 37 | config: 38 | WPDb: 39 | dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%' 40 | user: '%DB_USER%' 41 | password: '%DB_PASSWORD%' 42 | populator: 'mysql -u $user -p$password -h $host $dbname < $dump' 43 | dump: 'tests/_data/dump.sql' 44 | populate: true 45 | cleanup: true 46 | waitlock: 0 47 | url: '%WP_URL%' 48 | urlReplacement: true 49 | tablePrefix: '%WP_TABLE_PREFIX%' 50 | WPBrowser: 51 | url: '%WP_URL%' 52 | wpRootFolder: '%WP_ROOT_FOLDER%' 53 | adminUsername: '%ADMIN_USERNAME%' 54 | adminPassword: '%ADMIN_PASSWORD%' 55 | adminPath: '/wp-admin' 56 | cookies: false 57 | REST: 58 | depends: WPBrowser 59 | url: '%WP_URL%' 60 | WPFilesystem: 61 | wpRootFolder: '%WP_ROOT_FOLDER%' 62 | plugins: '/wp-content/plugins' 63 | mu-plugins: '/wp-content/mu-plugins' 64 | themes: '/wp-content/themes' 65 | uploads: '/wp-content/uploads' 66 | WPLoader: 67 | wpRootFolder: '%WP_ROOT_FOLDER%' 68 | dbName: '%DB_NAME%' 69 | dbHost: '%DB_HOST%' 70 | dbUser: '%DB_USER%' 71 | dbPassword: '%DB_PASSWORD%' 72 | tablePrefix: '%WP_TABLE_PREFIX%' 73 | domain: '%WP_DOMAIN%' 74 | adminEmail: '%ADMIN_EMAIL%' 75 | title: 'Test' 76 | plugins: 77 | - facetwp/index.php 78 | - wp-graphql/wp-graphql.php 79 | - wp-graphql-facetwp/wp-graphql-facetwp.php 80 | activatePlugins: 81 | - facetwp/index.php 82 | - wp-graphql/wp-graphql.php 83 | - wp-graphql-facetwp/wp-graphql-facetwp.php 84 | configFile: 'tests/_data/config.php' 85 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hsimah-services/wp-graphql-facetwp", 3 | "description": "WPGraphQL integration for FacetWP", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-3.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "AxePress Development", 9 | "email": "support@axepress.dev", 10 | "homepage": "https://axepress.dev" 11 | }, 12 | { 13 | "name": "David Levine", 14 | "role": "Lead Developer" 15 | }, 16 | { 17 | "name": "Hamish Blake", 18 | "role": "Original Developer", 19 | "email": "hsimah.services@gmail.com" 20 | } 21 | ], 22 | "support": { 23 | "email": "support@axepress.dev", 24 | "issues": "https://github.com/AxeWP/wp-graphql-facetwp/issues", 25 | "forum": "https://github.com/AxeWP/wp-graphql-facetwp/discussions", 26 | "source": "https://github.com/AxeWP/wp-graphql-facetwp" 27 | }, 28 | "readme": "README.md", 29 | "funding": [ 30 | { 31 | "type": "github", 32 | "url": "https://github.com/sponsors/AxeWP" 33 | } 34 | ], 35 | "config": { 36 | "platform": { 37 | "php": "7.4" 38 | }, 39 | "process-timeout": 0, 40 | "optimize-autoloader": true, 41 | "allow-plugins": { 42 | "dealerdirect/phpcodesniffer-composer-installer": true, 43 | "phpstan/extension-installer": true 44 | } 45 | }, 46 | "extra": { 47 | "strauss": { 48 | "target_directory": "vendor-prefixed", 49 | "namespace_prefix": "WPGraphQL\\FacetWP\\Vendor\\", 50 | "classmap_prefix": "GraphQL_FacetWP_Vendor", 51 | "constant_prefix": "GRAPHQL_FACETWP_VENDOR_", 52 | "delete_vendor_packages": true, 53 | "include_modified_date": false, 54 | "packages": [ 55 | "axepress/wp-graphql-plugin-boilerplate" 56 | ], 57 | "exclude_from_prefix": { 58 | "namespaces": [], 59 | "file_patterns": [] 60 | } 61 | } 62 | }, 63 | "autoload": { 64 | "files": [ 65 | "access-functions.php" 66 | ], 67 | "psr-4": { 68 | "WPGraphQL\\FacetWP\\": "src/" 69 | }, 70 | "classmap": [ 71 | "vendor-prefixed/" 72 | ] 73 | }, 74 | "autoload-dev": { 75 | "psr-4": { 76 | "Tests\\WPGraphQL\\FacetWP\\": "tests/_support/" 77 | } 78 | }, 79 | "require": { 80 | "php": ">=7.4", 81 | "axepress/wp-graphql-plugin-boilerplate": "^0.1.0" 82 | }, 83 | "require-dev": { 84 | "codeception/lib-innerbrowser": "^1.0", 85 | "codeception/module-asserts": "^1.0", 86 | "codeception/module-cli": "^1.0", 87 | "codeception/module-db": "^1.0", 88 | "codeception/module-filesystem": "^1.0", 89 | "codeception/module-phpbrowser": "^1.0", 90 | "codeception/module-rest": "^1.0", 91 | "codeception/module-webdriver": "^1.0", 92 | "codeception/phpunit-wrapper": "^9.0", 93 | "codeception/util-universalframework": "^1.0", 94 | "lucatume/wp-browser": "<3.5", 95 | "phpstan/phpstan": "^2.0.0", 96 | "phpstan/extension-installer": "^1.1", 97 | "szepeviktor/phpstan-wordpress": "^2.0.0", 98 | "wp-graphql/wp-graphql-testcase": "~3.4.0", 99 | "axepress/wp-graphql-stubs": "^2.0.0", 100 | "axepress/wp-graphql-cs": "^2.0.0", 101 | "wp-cli/wp-cli-bundle": "^2.8.1", 102 | "php-coveralls/php-coveralls": "^2.5", 103 | "phpcompatibility/php-compatibility": "dev-develop as 9.99.99" 104 | }, 105 | "scripts": { 106 | "install-test-env": "bash bin/install-test-env.sh", 107 | "install-stan-env": "bash bin/install-stan-env.sh", 108 | "docker-build": "bash bin/run-docker.sh build", 109 | "docker-run": "bash bin/run-docker.sh run", 110 | "docker-destroy": "docker compose down", 111 | "build-and-run": [ 112 | "@docker-build", 113 | "@docker-run" 114 | ], 115 | "build-app": "@docker-build -a", 116 | "build-test": "@docker-build -t", 117 | "run-app": "@docker-run -a", 118 | "run-test": "@docker-run -t", 119 | "strauss-install": [ 120 | "test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/download/0.19.1/strauss.phar" 121 | ], 122 | "strauss": [ 123 | "@strauss-install", 124 | "@php bin/strauss.phar", 125 | "composer dump-autoload --optimize" 126 | ], 127 | "pre-install-cmd": [ 128 | "test -d vendor-prefixed || mkdir vendor-prefixed" 129 | ], 130 | "pre-update-cmd": [ 131 | "@pre-install-cmd" 132 | ], 133 | "post-install-cmd": [ 134 | "@strauss" 135 | ], 136 | "post-update-cmd": [ 137 | "@strauss" 138 | ], 139 | "lint": "vendor/bin/phpcs", 140 | "phpcs-i": [ 141 | "php ./vendor/bin/phpcs -i" 142 | ], 143 | "check-cs": [ 144 | "php ./vendor/bin/phpcs" 145 | ], 146 | "fix-cs": [ 147 | "php ./vendor/bin/phpcbf" 148 | ], 149 | "phpstan": [ 150 | "vendor/bin/phpstan analyze --ansi --memory-limit=1G" 151 | ], 152 | "zip": [ 153 | "composer install --no-dev --optimize-autoloader", 154 | "mkdir -p plugin-build/wp-graphql-facetwp", 155 | "rsync -rc --exclude-from=.distignore --exclude=plugin-build . plugin-build/wp-graphql-facetwp/ --delete --delete-excluded -v", 156 | "cd plugin-build ; zip -r wp-graphql-facetwp.zip wp-graphql-facetwp", 157 | "rm -rf plugin-build/wp-graphql-facetwp/" 158 | ] 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | depends_on: 4 | - app_db 5 | image: wp-graphql-facetwp:latest-wp${WP_VERSION-6.7}-php${PHP_VERSION-8.2} 6 | volumes: 7 | - .:/var/www/html/wp-content/plugins/wp-graphql-facetwp 8 | - ./.log/app:/var/log/apache2 9 | env_file: 10 | - .env 11 | sysctls: 12 | - net.ipv4.ip_unprivileged_port_start=0 13 | environment: 14 | WP_URL: http://localhost:8091 15 | USING_XDEBUG: ${USING_XDEBUG:-} 16 | ports: 17 | - '8091:80' 18 | networks: 19 | local: 20 | 21 | app_db: 22 | image: mariadb:10.2 23 | environment: 24 | MYSQL_ROOT_PASSWORD: root 25 | MYSQL_DATABASE: wordpress 26 | MYSQL_USER: wordpress 27 | MYSQL_PASSWORD: wordpress 28 | ports: 29 | - '3306' 30 | networks: 31 | testing: 32 | local: 33 | 34 | testing: 35 | depends_on: 36 | - app_db 37 | image: wp-graphql-facetwp-testing:latest-wp${WP_VERSION-6.7}-php${PHP_VERSION-8.1} 38 | volumes: 39 | - .:/var/www/html/wp-content/plugins/wp-graphql-facetwp 40 | - ./.log/testing:/var/log/apache2 41 | env_file: 42 | - .env 43 | environment: 44 | SUITES: ${SUITES:-} 45 | sysctls: 46 | - net.ipv4.ip_unprivileged_port_start=0 47 | networks: 48 | testing: 49 | 50 | networks: 51 | local: 52 | testing: 53 | -------------------------------------------------------------------------------- /docker/app.Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Pre-configured WordPress Installation w/ WPGraphQL, FacetWP, and WPGraphQL for FacetWP # 3 | # For testing only, use in production not recommended. # 4 | # Forked from https://github.com/AxeWP/wp-graphql-plugin-boilerplate/blob/d2f488496e0cd08c34619b4bb8d5cdd2aa640347/wp-graphql-plugin-name/docker/app.Dockerfile 5 | ############################################################################### 6 | 7 | # Use build args to get the right wordpress + php image 8 | ARG WP_VERSION 9 | ARG PHP_VERSION 10 | 11 | FROM wordpress:${WP_VERSION}-php${PHP_VERSION}-apache 12 | 13 | # Needed to specify the build args again after the FROM command. 14 | ARG WP_VERSION 15 | ARG PHP_VERSION 16 | 17 | # Save the build args for use by the runtime environment 18 | ENV WP_VERSION=${WP_VERSION} 19 | ENV PHP_VERSION=${PHP_VERSION} 20 | 21 | LABEL author=axepress 22 | LABEL author_uri=https://github.com/axewp 23 | 24 | SHELL [ "/bin/bash", "-c" ] 25 | 26 | # Install system packages 27 | RUN apt-get update && \ 28 | apt-get -y install \ 29 | # CircleCI depedencies 30 | git \ 31 | ssh \ 32 | tar \ 33 | gzip \ 34 | wget \ 35 | mariadb-client 36 | 37 | # Install Dockerize 38 | ENV DOCKERIZE_VERSION v0.6.1 39 | RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ 40 | && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ 41 | && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz 42 | 43 | # Install WP-CLI 44 | RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \ 45 | && chmod +x wp-cli.phar \ 46 | && mv wp-cli.phar /usr/local/bin/wp 47 | 48 | # Set project environmental variables 49 | ENV WP_ROOT_FOLDER="/var/www/html" 50 | ENV PLUGINS_DIR="${WP_ROOT_FOLDER}/wp-content/plugins" 51 | ENV PROJECT_DIR="${PLUGINS_DIR}/wp-graphql-facetwp" 52 | ENV DATA_DUMP_DIR="${PROJECT_DIR}/tests/_data" 53 | 54 | # Remove exec statement from base entrypoint script. 55 | RUN sed -i '$d' /usr/local/bin/docker-entrypoint.sh 56 | 57 | # Set up Apache 58 | RUN echo 'ServerName localhost' >> /etc/apache2/apache2.conf 59 | 60 | # Custom PHP settings 61 | RUN echo "upload_max_filesize = 50M" >> /usr/local/etc/php/conf.d/custom.ini \ 62 | ; 63 | 64 | # Install XDebug 3 65 | RUN echo "Installing XDebug 3 version $XDEBUG_VERSION (in disabled state)" \ 66 | && if [[ $PHP_VERSION == 7* ]]; then pecl install xdebug-3.1.5; else pecl install xdebug; fi \ 67 | && mkdir -p /usr/local/etc/php/conf.d/disabled \ 68 | && echo "zend_extension=xdebug" > /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini \ 69 | && echo "xdebug.mode=develop,debug,coverage" >> /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini \ 70 | && echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini \ 71 | && echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini \ 72 | && echo "xdebug.client_port=9003" >> /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini \ 73 | && echo "xdebug.max_nesting_level=512" >> /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini \ 74 | ; 75 | 76 | # Set xdebug configuration off by default. See the entrypoint.sh. 77 | ENV USING_XDEBUG=0 78 | 79 | # Set up entrypoint 80 | WORKDIR /var/www/html 81 | COPY docker/app.setup.sh /usr/local/bin/app-setup.sh 82 | COPY docker/app.post-setup.sh /usr/local/bin/app-post-setup.sh 83 | COPY docker/app.entrypoint.sh /usr/local/bin/app-entrypoint.sh 84 | RUN chmod 755 /usr/local/bin/app-entrypoint.sh 85 | ENTRYPOINT ["app-entrypoint.sh"] 86 | CMD ["apache2-foreground"] 87 | -------------------------------------------------------------------------------- /docker/app.entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run app setup script. 4 | . app-setup.sh 5 | . app-post-setup.sh 6 | 7 | exec "$@" -------------------------------------------------------------------------------- /docker/app.post-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | install_facetwp() { 4 | if [ ! -d $WP_CORE_DIR/wp-content/plugins/facetwp ]; then 5 | echo "Cloning FacetWP: https://${GIT_TOKEN}@${FACET_REPO}" 6 | git clone -b master --single-branch https://${GIT_TOKEN}@${FACET_REPO} $PLUGINS_DIR/facetwp 7 | fi 8 | echo "Activating FacetWP" 9 | wp plugin activate facetwp --allow-root 10 | } 11 | 12 | # Install plugins 13 | install_facetwp 14 | 15 | # Install WPGraphQL and Activate 16 | if ! $( wp plugin is-installed wp-graphql --allow-root ); then 17 | wp plugin install wp-graphql --allow-root 18 | fi 19 | wp plugin activate wp-graphql --allow-root 20 | 21 | # activate the plugin 22 | wp plugin activate wp-graphql-facetwp --allow-root 23 | 24 | # Set pretty permalinks. 25 | wp rewrite structure '/%year%/%monthnum%/%postname%/' --allow-root 26 | 27 | # Export the db for codeception to use 28 | wp db export "${DATA_DUMP_DIR}/dump.sql" --allow-root 29 | 30 | # If maintenance mode is active, de-activate it 31 | if $(wp maintenance-mode is-active --allow-root); then 32 | echo "Deactivating maintenance mode" 33 | wp maintenance-mode deactivate --allow-root 34 | fi 35 | 36 | chmod 777 -R . 37 | chown -R $(id -u):$(id -g) . 38 | -------------------------------------------------------------------------------- /docker/app.setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$USING_XDEBUG" == "1" ]; then 4 | echo "Enabling XDebug 3" 5 | mv /usr/local/etc/php/conf.d/disabled/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/ 6 | fi 7 | 8 | # Run WordPress docker entrypoint. 9 | . docker-entrypoint.sh 'apache2' 10 | 11 | set +u 12 | 13 | # Ensure mysql is loaded 14 | dockerize -wait tcp://${DB_HOST}:${DB_HOST_PORT:-3306} -timeout 1m 15 | 16 | # Config WordPress 17 | if [ ! -f "${WP_ROOT_FOLDER}/wp-config.php" ]; then 18 | wp config create \ 19 | --path="${WP_ROOT_FOLDER}" \ 20 | --dbname="${DB_NAME}" \ 21 | --dbuser="${DB_USER}" \ 22 | --dbpass="${DB_PASSWORD}" \ 23 | --dbhost="${DB_HOST}" \ 24 | --dbprefix="${WP_TABLE_PREFIX}" \ 25 | --skip-check \ 26 | --quiet \ 27 | --allow-root 28 | fi 29 | 30 | wp config set WP_AUTO_UPDATE_CORE false --raw --allow-root 31 | wp config set AUTOMATIC_UPDATER_DISABLED true --raw --allow-root 32 | wp config set FS_METHOD direct --allow-root 33 | 34 | # Install WP if not yet installed 35 | if ! $(wp core is-installed --allow-root); then 36 | wp core install \ 37 | --path="${WP_ROOT_FOLDER}" \ 38 | --url="${WP_URL}" \ 39 | --title='Test' \ 40 | --admin_user="${ADMIN_USERNAME}" \ 41 | --admin_password="${ADMIN_PASSWORD}" \ 42 | --admin_email="${ADMIN_EMAIL}" \ 43 | --allow-root 44 | fi 45 | 46 | echo "Running WordPress version: $(wp core version --allow-root) at $(wp option get home --allow-root)" 47 | -------------------------------------------------------------------------------- /docker/testing.Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Container for running Codeception tests on a WPGraphQL Docker instance. # 3 | ############################################################################ 4 | 5 | ARG WP_VERSION 6 | ARG PHP_VERSION 7 | 8 | FROM wp-graphql-facetwp:latest-wp${WP_VERSION}-php${PHP_VERSION} 9 | 10 | LABEL author=axepress 11 | LABEL author_uri=https://github.com/AxeWP 12 | 13 | SHELL [ "/bin/bash", "-c" ] 14 | 15 | # Install php extensions 16 | RUN docker-php-ext-install pdo_mysql 17 | 18 | # Install PCOV 19 | # This is needed for Codeception / PHPUnit to track code coverage 20 | RUN apt-get install zip unzip -y \ 21 | && pecl install pcov 22 | 23 | ENV COVERAGE= 24 | ENV SUITES=${SUITES:-} 25 | 26 | # Install composer 27 | ENV COMPOSER_ALLOW_SUPERUSER=1 28 | 29 | RUN curl -sS https://getcomposer.org/installer | php -- \ 30 | --filename=composer \ 31 | --install-dir=/usr/local/bin 32 | 33 | # Add composer global binaries to PATH 34 | ENV PATH "$PATH:~/.composer/vendor/bin" 35 | 36 | # Configure php 37 | RUN echo "date.timezone = UTC" >> /usr/local/etc/php/php.ini 38 | 39 | # Set up entrypoint 40 | WORKDIR /var/www/html 41 | COPY docker/testing.entrypoint.sh /usr/local/bin/testing-entrypoint.sh 42 | RUN chmod 755 /usr/local/bin/testing-entrypoint.sh 43 | ENTRYPOINT ["testing-entrypoint.sh"] 44 | -------------------------------------------------------------------------------- /docker/testing.entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "WordPress: ${WP_VERSION} PHP: ${PHP_VERSION}" 4 | 5 | # Processes parameters and runs Codeception. 6 | run_tests() { 7 | if [[ -n "$COVERAGE" ]]; then 8 | local coverage="--coverage --coverage-xml" 9 | fi 10 | if [[ -n "$DEBUG" ]]; then 11 | local debug="--debug" 12 | fi 13 | 14 | local suites=$1 15 | if [[ -z "$suites" ]]; then 16 | echo "No test suites specified. Must specify variable SUITES." 17 | exit 1 18 | fi 19 | 20 | # If maintenance mode is active, de-activate it 21 | if $( wp maintenance-mode is-active --allow-root ); then 22 | echo "Deactivating maintenance mode" 23 | wp maintenance-mode deactivate --allow-root 24 | fi 25 | 26 | 27 | # Suites is the comma separated list of suites/tests to run. 28 | echo "Running Test Suite $suites" 29 | vendor/bin/codecept run -c codeception.dist.yml "${suites}" ${coverage:-} ${debug:-} --no-exit 30 | } 31 | 32 | # Exits with a status of 0 (true) if provided version number is higher than proceeding numbers. 33 | version_gt() { 34 | test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; 35 | } 36 | 37 | write_htaccess() { 38 | echo " 39 | RewriteEngine On 40 | RewriteBase / 41 | SetEnvIf Authorization \"(.*)\" HTTP_AUTHORIZATION=\$1 42 | RewriteRule ^index\.php$ - [L] 43 | RewriteCond %{REQUEST_FILENAME} !-f 44 | RewriteCond %{REQUEST_FILENAME} !-d 45 | RewriteRule . /index.php [L] 46 | " >> ${WP_ROOT_FOLDER}/.htaccess 47 | } 48 | 49 | # Move to WordPress root folder 50 | workdir="$PWD" 51 | echo "Moving to WordPress root directory ${WP_ROOT_FOLDER}." 52 | cd ${WP_ROOT_FOLDER} 53 | 54 | # Because we are starting apache independetly of the docker image, 55 | # we set WORDPRESS environment variables so apache see them and used in the wp-config.php 56 | echo "export WORDPRESS_DB_HOST=${WORDPRESS_DB_HOST}" >> /etc/apache2/envvars 57 | echo "export WORDPRESS_DB_USER=${WORDPRESS_DB_USER}" >> /etc/apache2/envvars 58 | echo "export WORDPRESS_DB_PASSWORD=${WORDPRESS_DB_PASSWORD}" >> /etc/apache2/envvars 59 | echo "export WORDPRESS_DB_NAME=${WORDPRESS_DB_NAME}" >> /etc/apache2/envvars 60 | 61 | # Run app setup scripts. 62 | . app-setup.sh 63 | . app-post-setup.sh 64 | 65 | write_htaccess 66 | 67 | # Return to PWD. 68 | echo "Moving back to project working directory ${PROJECT_DIR}" 69 | cd ${PROJECT_DIR} 70 | 71 | # Ensure Apache is running 72 | service apache2 start 73 | 74 | # Ensure everything is loaded 75 | dockerize \ 76 | -wait tcp://${DB_HOST}:${DB_HOST_PORT:-3306} \ 77 | -wait ${WP_URL} \ 78 | -timeout 1m 79 | 80 | # Download c3 for testing. 81 | if [ ! -f "$PROJECT_DIR/c3.php" ]; then 82 | echo "Downloading Codeception's c3.php" 83 | curl -L 'https://raw.github.com/Codeception/c3/2.0/c3.php' > "$PROJECT_DIR/c3.php" 84 | fi 85 | 86 | echo "Running composer install" 87 | COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction 88 | 89 | 90 | # Install pcov/clobber if PHP7.1+ 91 | if version_gt $PHP_VERSION 7.0 && [[ -n "$COVERAGE" ]] && [[ -z "$USING_XDEBUG" ]]; then 92 | echo "Using pcov/clobber for codecoverage" 93 | docker-php-ext-enable pcov 94 | echo "pcov.enabled=1" >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini 95 | echo "pcov.directory = ${PROJECT_DIR}" >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini 96 | COMPOSER_MEMORY_LIMIT=-1 composer require pcov/clobber --dev 97 | vendor/bin/pcov clobber 98 | elif [[ -n "$COVERAGE" ]] && [[ -n "$USING_XDEBUG" ]]; then 99 | echo "Using XDebug for codecoverage" 100 | fi 101 | 102 | # Set output permission 103 | echo "Setting Codeception output directory permissions" 104 | chmod 777 ${TESTS_OUTPUT} 105 | 106 | # Run tests 107 | run_tests "${SUITES}" 108 | 109 | # Remove c3.php 110 | if [ -f "$PROJECT_DIR/c3.php" ] && [ "$SKIP_TESTS_CLEANUP" != "1" ]; then 111 | echo "Removing Codeception's c3.php" 112 | rm -rf "$PROJECT_DIR/c3.php" 113 | fi 114 | 115 | # Clean coverage.xml and clean up PCOV configurations. 116 | if [ -f "${TESTS_OUTPUT}/coverage.xml" ] && [[ -n "$COVERAGE" ]]; then 117 | echo 'Cleaning coverage.xml for deployment'. 118 | pattern="$PROJECT_DIR/" 119 | sed -i "s~$pattern~~g" "$TESTS_OUTPUT"/coverage.xml 120 | 121 | # Remove pcov/clobber 122 | if version_gt $PHP_VERSION 7.0 && [[ -z "$SKIP_TESTS_CLEANUP" ]] && [[ -z "$USING_XDEBUG" ]]; then 123 | echo 'Removing pcov/clobber.' 124 | vendor/bin/pcov unclobber 125 | COMPOSER_MEMORY_LIMIT=-1 composer remove --dev pcov/clobber 126 | rm /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini 127 | fi 128 | 129 | fi 130 | 131 | if [[ -z "$SKIP_TESTS_CLEANUP" ]]; then 132 | echo 'Changing composer configuration in container.' 133 | composer config --global discard-changes true 134 | 135 | echo 'Removing devDependencies.' 136 | composer install --no-dev -n 137 | 138 | echo 'Removing composer.lock' 139 | rm composer.lock 140 | fi 141 | 142 | # Set public test result files permissions. 143 | if [ -n "$(ls "$TESTS_OUTPUT")" ]; then 144 | echo 'Setting result files permissions'. 145 | chmod 777 -R "$TESTS_OUTPUT"/* 146 | fi 147 | 148 | 149 | # Check results and exit accordingly. 150 | if [ -f "${TESTS_OUTPUT}/failed" ]; then 151 | echo "Uh oh, something went wrong." 152 | exit 1 153 | else 154 | echo "Woohoo! It's working!" 155 | fi 156 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AxeWP/wp-graphql-facetwp/202a9248a312cd2bb6aaccc17f19cc3d8d36b637/logo.png -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | checkExplicitMixedMissingReturn: true 4 | checkFunctionNameCase: true 5 | checkInternalClassCaseSensitivity: true 6 | checkTooWideReturnTypesInProtectedAndPublicMethods: true 7 | inferPrivatePropertyTypeFromConstructor: true 8 | polluteScopeWithAlwaysIterableForeach: false 9 | polluteScopeWithLoopInitialAssignments: false 10 | reportAlwaysTrueInLastCondition: true 11 | reportStaticMethodSignatures: true 12 | reportWrongPhpDocTypeInVarTag: true 13 | treatPhpDocTypesAsCertain: false 14 | stubFiles: 15 | # Simulate added properties 16 | - phpstan/class-facetwp.php 17 | - phpstan/class-facetwp-facet.php 18 | - phpstan/class-wp-post-type.php 19 | bootstrapFiles: 20 | - phpstan/constants.php 21 | - wp-graphql-facetwp.php 22 | paths: 23 | - wp-graphql-facetwp.php 24 | - src/ 25 | excludePaths: 26 | analyse: 27 | - vendor-prefixed 28 | - vendor 29 | scanDirectories: 30 | - ../wp-graphql/ 31 | - ../facetwp/ 32 | ignoreErrors: 33 | - 34 | message: '#Static method WPGraphQL\\FacetWP\\Registry\\FacetRegistry::register_facet_settings\(\) is unused\.#' 35 | path: src/Registry/FacetRegistry.php 36 | -------------------------------------------------------------------------------- /phpstan/class-facetwp-facet.php: -------------------------------------------------------------------------------- 1 | $fields The facet settings fields. 8 | */ 9 | class FacetWP_Facet {} 10 | -------------------------------------------------------------------------------- /phpstan/class-facetwp.php: -------------------------------------------------------------------------------- 1 | wp-graphql-facetwp.zip'; 67 | 68 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 69 | error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a development notice. 70 | sprintf( 71 | esc_html( $error_message ), 72 | wp_kses( 73 | $release_link, 74 | [ 75 | 'a' => [ 76 | 'href' => [], 77 | 'target' => [], 78 | ], 79 | ] 80 | ) 81 | ) 82 | ); 83 | } 84 | 85 | $hooks = [ 86 | 'admin_notices', 87 | 'network_admin_notices', 88 | ]; 89 | 90 | foreach ( $hooks as $hook ) { 91 | add_action( 92 | $hook, 93 | static function () use ( $error_message, $release_link ) { 94 | ?> 95 |
96 |

97 | [ 104 | 'href' => [], 105 | 'target' => [], 106 | ], 107 | ] 108 | ) 109 | ) 110 | ?> 111 |

112 |
113 | setup(); 34 | } 35 | 36 | /** 37 | * Fire off init action. 38 | * 39 | * @param self $instance the instance of the plugin class. 40 | */ 41 | do_action( 'graphql_facetwp_init', self::$instance ); 42 | 43 | return self::$instance; 44 | } 45 | 46 | /** 47 | * Sets up the schema. 48 | * 49 | * @codeCoverageIgnore 50 | */ 51 | private function setup(): void { 52 | // Setup boilerplate hook prefix. 53 | Helper::set_hook_prefix( 'graphql_facetwp' ); 54 | 55 | // Initialize plugin type registry. 56 | TypeRegistry::init(); 57 | CoreSchemaFilters::init(); 58 | } 59 | 60 | /** 61 | * Throw error on object clone. 62 | * The whole idea of the singleton design pattern is that there is a single object 63 | * therefore, we don't want the object to be cloned. 64 | * 65 | * @codeCoverageIgnore 66 | * 67 | * @return void 68 | */ 69 | public function __clone() { 70 | // Cloning instances of the class is forbidden. 71 | _doing_it_wrong( __FUNCTION__, esc_html__( 'The WPGraphQL\FacetWP\Main class should not be cloned.', 'wpgraphql-facetwp' ), '0.0.1' ); 72 | } 73 | 74 | /** 75 | * Disable unserializing of the class. 76 | * 77 | * @codeCoverageIgnore 78 | */ 79 | public function __wakeup(): void { 80 | // De-serializing instances of the class is forbidden. 81 | _doing_it_wrong( __FUNCTION__, esc_html__( 'De-serializing instances of the WPGraphQL\FacetWP\Main class is not allowed', 'wpgraphql-facetwp' ), '0.0.1' ); 82 | } 83 | } 84 | endif; 85 | -------------------------------------------------------------------------------- /src/Registry/FacetRegistry.php: -------------------------------------------------------------------------------- 1 | [] 24 | */ 25 | protected static $facets; 26 | 27 | /** 28 | * Gets the facet configs to be registered to WPGraphQL. 29 | * 30 | * @return array[] 31 | * 32 | * @since 0.4.1 33 | */ 34 | public static function get_allowed_facets(): array { 35 | if ( ! isset( self::$facets ) ) { 36 | $configs = FWP()->helper->get_facets(); 37 | 38 | // Add GraphQL properties to each facet config. 39 | foreach ( $configs as $key => $config ) { 40 | // @todo set these on the backend. 41 | $configs[ $key ]['graphql_field_name'] = $config['graphql_field_name'] ?? graphql_format_field_name( $config['name'] ); 42 | $configs[ $key ]['show_in_graphql'] = $config['show_in_graphql'] ?? true; 43 | $configs[ $key ]['graphql_type'] = self::get_facet_input_type( $config ); 44 | } 45 | 46 | self::$facets = array_values( 47 | array_filter( 48 | $configs, 49 | static function ( $config ) { 50 | return $config['show_in_graphql']; 51 | } 52 | ) 53 | ); 54 | } 55 | 56 | return self::$facets; 57 | } 58 | 59 | /** 60 | * Gets the GraphQL input type for a facet. 61 | * 62 | * @param array $config The facet config. 63 | * 64 | * @return string|array The GraphQL input type. 65 | * 66 | * @since 0.4.1 67 | */ 68 | public static function get_facet_input_type( array $config ) { 69 | // The default facet type is a list of strings. 70 | $type = [ 'list_of' => 'String' ]; 71 | 72 | switch ( $config['type'] ) { 73 | case 'fselect': 74 | // Single string for single fSelect. 75 | if ( 'yes' === $config['multiple'] ) { 76 | break; 77 | } 78 | // Continuing... 79 | case 'radio': 80 | case 'search': 81 | // Single string. 82 | $type = 'String'; 83 | 84 | break; 85 | case 'date_range': 86 | // Custom payload. 87 | $type = Input\DateRangeArgs::get_type_name(); 88 | 89 | break; 90 | case 'number_range': 91 | // Custom payload. 92 | $type = Input\NumberRangeArgs::get_type_name(); 93 | 94 | break; 95 | case 'slider': 96 | // Custom payload. 97 | $type = Input\SliderArgs::get_type_name(); 98 | 99 | break; 100 | case 'proximity': 101 | // Custom payload. 102 | $type = Input\ProximityArgs::get_type_name(); 103 | 104 | break; 105 | case 'rating': 106 | // Single Int. 107 | $type = 'Int'; 108 | 109 | break; 110 | case 'sort': 111 | $type = SortOptionsEnum::get_type_name( $config['name'] ); 112 | 113 | break; 114 | case 'autocomplete': 115 | case 'checkboxes': 116 | case 'dropdown': 117 | case 'hierarchy': 118 | default: 119 | // String array - default. 120 | break; 121 | } 122 | 123 | /** 124 | * Filter the GraphQL input type for a facet. 125 | * 126 | * @param string|array $input_type The GraphQL Input type name to use. 127 | * @param string $facet_type The FacetWP facet type. 128 | * @param array $facet_config The FacetWP facet config. 129 | */ 130 | $type = apply_filters( 'graphql_facetwp_facet_input_type', $type, $config['type'], $config ); 131 | 132 | return $type; 133 | } 134 | 135 | /** 136 | * Register WPGraphQL Facet query. 137 | * 138 | * @todo Move to type registry. 139 | * 140 | * @param string $type The Post Type name. 141 | */ 142 | public static function register( string $type ): void { 143 | $post_type = get_post_type_object( $type ); 144 | 145 | if ( null === $post_type || ! $post_type->show_in_graphql ) { 146 | return; 147 | } 148 | 149 | $config = [ 150 | 'type' => $type, 151 | 'singular' => $post_type->graphql_single_name, 152 | 'plural' => $post_type->graphql_plural_name, 153 | 'field' => $post_type->graphql_single_name . 'Facet', 154 | ]; 155 | 156 | self::register_root_field( $config ); 157 | self::register_input_arg_types( $config ); 158 | self::register_custom_output_types( $config ); 159 | self::register_facet_connection( $config ); 160 | } 161 | 162 | /** 163 | * Register facet-type root field. 164 | * 165 | * @param array $facet_config The config array. 166 | */ 167 | private static function register_root_field( array $facet_config ): void { 168 | $type = $facet_config['type']; 169 | $singular = $facet_config['singular']; 170 | $field = $facet_config['field']; 171 | 172 | $use_graphql_pagination = self::use_graphql_pagination(); 173 | 174 | register_graphql_field( 175 | 'RootQuery', 176 | $field, 177 | [ 178 | 'type' => $field, 179 | 'description' => sprintf( 180 | // translators: The GraphQL singular type name. 181 | __( 'The %s FacetWP Query', 'wpgraphql-facetwp' ), 182 | $singular 183 | ), 184 | 'args' => [ 185 | 'where' => [ 186 | 'type' => $field . 'WhereArgs', 187 | 'description' => sprintf( 188 | // translators: The GraphQL Field name. 189 | __( 'Arguments for %s query', 'wpgraphql-facetwp' ), 190 | $field 191 | ), 192 | ], 193 | ], 194 | 'resolve' => static function ( $source, array $args ) use ( $type, $use_graphql_pagination ) { 195 | $where = $args['where']; 196 | 197 | $pagination = [ 198 | 'per_page' => 10, 199 | 'page' => 1, 200 | ]; 201 | 202 | if ( ! empty( $where['pager'] ) ) { 203 | $pagination = array_merge( $pagination, $where['pager'] ); 204 | } 205 | 206 | $query = ! empty( $where['query'] ) ? $where['query'] : []; 207 | $query = self::parse_query( $query ); 208 | 209 | // Clean up null args. 210 | foreach ( $query as $key => $value ) { 211 | if ( ! $value ) { 212 | $query[ $key ] = []; 213 | } 214 | } 215 | 216 | $fwp_args = [ 217 | 'facets' => $query, 218 | 'query_args' => [ 219 | 'post_type' => $type, 220 | 'post_status' => ! empty( $where['status'] ) ? $where['status'] : 'publish', 221 | 'posts_per_page' => (int) $pagination['per_page'], 222 | 'paged' => (int) $pagination['page'], 223 | ], 224 | ]; 225 | 226 | // Stash the sort settings, since we don't get them from the payload. 227 | $sort_settings = []; 228 | 229 | // Apply the orderby args. 230 | foreach ( $fwp_args['facets'] as $key => $facet_args ) { 231 | if ( ! empty( $facet_args['is_sort'] ) ) { 232 | $fwp_args['query_args'] = array_merge_recursive( $fwp_args['query_args'], $facet_args['query_args'] ); 233 | $sort_settings[ $key ] = $facet_args['settings']; 234 | 235 | // Set the selected facet back to a string. 236 | $fwp_args['facets'][ $key ] = $facet_args['selected']; 237 | } 238 | } 239 | 240 | $filtered_ids = []; 241 | if ( $use_graphql_pagination ) { 242 | 243 | // TODO find a better place to register this handler. 244 | add_filter( 245 | 'facetwp_filtered_post_ids', 246 | static function ( $post_ids ) use ( &$filtered_ids ) { 247 | $filtered_ids = $post_ids; 248 | return $post_ids; 249 | }, 250 | 10, 251 | 1 252 | ); 253 | } 254 | 255 | $payload = FWP()->api->process_request( $fwp_args ); 256 | 257 | $results = $payload['results']; 258 | if ( $use_graphql_pagination ) { 259 | $results = $filtered_ids; 260 | } 261 | 262 | // @todo helper function. 263 | foreach ( $payload['facets'] as $key => $facet ) { 264 | // Try to get the settings from the payload, otherwise fallback to the parsed query args. 265 | if ( isset( $facet['settings'] ) ) { 266 | $facet['settings'] = self::to_camel_case( $facet['settings'] ); 267 | } elseif ( isset( $sort_settings[ $key ] ) ) { 268 | $facet['settings'] = self::to_camel_case( $sort_settings[ $key ] ); 269 | } 270 | 271 | $payload['facets'][ $key ] = $facet; 272 | } 273 | 274 | /** 275 | * The facets array is the resolved payload for this field. 276 | * Results & pager are returned so the connection resolver can use the data. 277 | */ 278 | return [ 279 | 'facets' => array_values( $payload['facets'] ), 280 | 'results' => count( $results ) ? $results : null, 281 | 'pager' => $payload['pager'] ?? [], 282 | 'is_sort' => ! empty( $fwp_args['query_args']['orderby'] ), 283 | ]; 284 | }, 285 | ] 286 | ); 287 | } 288 | 289 | /** 290 | * Register input argument types. 291 | * 292 | * @param array $facet_config The config array. 293 | */ 294 | private static function register_input_arg_types( array $facet_config ): void { 295 | $field = $facet_config['field']; 296 | 297 | $use_graphql_pagination = self::use_graphql_pagination(); 298 | 299 | $facets = self::get_allowed_facets(); 300 | 301 | $field_configs = array_reduce( 302 | $facets, 303 | static function ( $prev, $cur ) { 304 | if ( empty( $cur['graphql_field_name'] ) ) { 305 | return $prev; 306 | } 307 | 308 | // Add the field config. 309 | $prev[ $cur['graphql_field_name'] ] = [ 310 | 'type' => $cur['graphql_type'], 311 | 'description' => sprintf( 312 | // translators: The current Facet label. 313 | __( '%s facet query', 'wpgraphql-facetwp' ), 314 | $cur['label'] 315 | ), 316 | ]; 317 | 318 | // Maybe add the deprecate type name. 319 | if ( $cur['name'] !== $cur['graphql_field_name'] ) { 320 | $prev[ $cur['name'] ] = [ 321 | 'type' => $cur['graphql_type'], 322 | 'description' => sprintf( 323 | // translators: The current Facet label. 324 | __( 'DEPRECATED since 0.4.1', 'wpgraphql-facetwp' ), 325 | $cur['label'] 326 | ), 327 | 'deprecationReason' => sprintf( 328 | // translators: The the GraphQL field name. 329 | __( 'Use %s instead.', 'wpgraphql-facetwp' ), 330 | $cur['graphql_field_name'] 331 | ), 332 | ]; 333 | } 334 | 335 | return $prev; 336 | }, 337 | [] 338 | ); 339 | 340 | register_graphql_input_type( 341 | 'FacetQueryArgs', 342 | [ 343 | 'description' => sprintf( 344 | // translators: The GraphQL Field name. 345 | __( 'Seleted facets for %s query', 'wpgraphql-facetwp' ), 346 | $field 347 | ), 348 | 'fields' => $field_configs, 349 | ] 350 | ); 351 | 352 | if ( ! $use_graphql_pagination ) { 353 | register_graphql_input_type( 354 | $field . 'Pager', 355 | [ 356 | 'description' => __( 357 | 'FacetWP Pager input type.', 358 | 'wpgraphql-facetwp' 359 | ), 360 | 'fields' => [ 361 | 'per_page' => [ 362 | 'type' => 'Int', 363 | 'description' => __( 364 | 'Number of post to show per page. Passed to posts_per_page of WP_Query.', 365 | 'wpgraphql-facetwp' 366 | ), 367 | ], 368 | 'page' => [ 369 | 'type' => 'Int', 370 | 'description' => __( 371 | 'The page to fetch.', 372 | 'wpgraphql-facetwp' 373 | ), 374 | ], 375 | ], 376 | ] 377 | ); 378 | } 379 | 380 | $where_fields = [ 381 | 'status' => [ 382 | 'type' => 'PostStatusEnum', 383 | 'description' => __( 'The post status.', 'wpgraphql-facetwp' ), 384 | ], 385 | 'query' => [ 386 | 'type' => 'FacetQueryArgs', 387 | 'description' => __( 'The FacetWP query args.', 'wpgraphql-facetwp' ), 388 | ], 389 | ]; 390 | 391 | if ( ! $use_graphql_pagination ) { 392 | $where_fields['pager'] = [ 393 | 'type' => $field . 'Pager', 394 | 'description' => __( 'The FacetWP pager args.', 'wpgraphql-facetwp' ), 395 | ]; 396 | } 397 | 398 | register_graphql_input_type( 399 | $field . 'WhereArgs', 400 | [ 401 | 'description' => sprintf( 402 | // translators: The GraphQL Field name. 403 | __( 'Arguments for %s query', 'wpgraphql-facetwp' ), 404 | $field 405 | ), 406 | 'fields' => $where_fields, 407 | ] 408 | ); 409 | } 410 | 411 | /** 412 | * Register custom output types. 413 | * 414 | * @param array $facet_config The config array. 415 | */ 416 | private static function register_custom_output_types( array $facet_config ): void { 417 | $singular = $facet_config['singular']; 418 | $field = $facet_config['field']; 419 | 420 | $use_graphql_pagination = self::use_graphql_pagination(); 421 | 422 | $fields = [ 423 | 'facets' => [ 424 | 'type' => [ 'list_of' => 'Facet' ], 425 | 'description' => __( 'The facets for this query.', 'wpgraphql-facetwp' ), 426 | ], 427 | ]; 428 | 429 | if ( ! $use_graphql_pagination ) { 430 | $fields['pager'] = [ 431 | 'type' => 'FacetPager', 432 | 'description' => __( 'The FacetWP pager for this query.', 'wpgraphql-facetwp' ), 433 | ]; 434 | } 435 | 436 | register_graphql_object_type( 437 | $field, 438 | [ 439 | 'description' => sprintf( 440 | // translators: The GraphQL singular type name. 441 | __( '%s FacetWP Payload', 'wpgraphql-facetwp' ), 442 | $singular 443 | ), 444 | 'fields' => $fields, 445 | ] 446 | ); 447 | } 448 | 449 | /** 450 | * Register facet-type connection types. 451 | * 452 | * @param array $facet_config The config array. 453 | */ 454 | private static function register_facet_connection( array $facet_config ): void { 455 | $type = $facet_config['type']; 456 | $singular = $facet_config['singular']; 457 | $field = $facet_config['field']; 458 | $plural = $facet_config['plural']; 459 | 460 | $use_graphql_pagination = self::use_graphql_pagination(); 461 | 462 | $default_facet_connection_config = [ 463 | 'fromType' => $field, 464 | 'toType' => $singular, 465 | 'fromFieldName' => lcfirst( $plural ), 466 | 'connectionArgs' => PostObjects::get_connection_args(), 467 | 'resolveNode' => static function ( $node, $_args, $context ) { 468 | return $context->get_loader( 'post' )->load_deferred( $node->ID ); 469 | }, 470 | 'resolve' => static function ( $source, $args, $context, $info ) use ( $type, $use_graphql_pagination ) { 471 | if ( ! $use_graphql_pagination ) { 472 | // Manually override the first query arg if per_page > 10, the first default value. 473 | $args['first'] = $source['pager']['per_page']; 474 | } 475 | 476 | $resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, $type ); 477 | 478 | if ( ! empty( $source['results'] ) ) { 479 | $resolver->set_query_arg( 'post__in', $source['results'] ); 480 | } 481 | 482 | // Use post__in when delegating sorting to FWP. 483 | if ( ! empty( $source['is_sort'] ) ) { 484 | $resolver->set_query_arg( 'orderby', 'post__in' ); 485 | } 486 | 487 | return $resolver->get_connection(); 488 | }, 489 | ]; 490 | 491 | /** 492 | * @param array $connection_config The connection config array. 493 | * @param array $facet_config The facet data array used to generate the config. 494 | */ 495 | $graphql_connection_config = apply_filters( 496 | 'facetwp_graphql_facet_connection_config', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 497 | $default_facet_connection_config, 498 | $facet_config 499 | ); 500 | 501 | register_graphql_connection( $graphql_connection_config ); 502 | } 503 | 504 | /** 505 | * Parse WPGraphQL query into FacetWP query 506 | * 507 | * @param array $query The WPGraphQL query. 508 | * 509 | * @return array The FacetWP query. 510 | */ 511 | private static function parse_query( array $query ): array { 512 | // Bail early if no query set. 513 | if ( empty( $query ) ) { 514 | return []; 515 | } 516 | 517 | $facets = FWP()->helper->get_facets(); 518 | 519 | $reduced_query = array_reduce( 520 | $facets, 521 | static function ( $prev, $cur ) use ( $query ) { 522 | // Get the facet name. 523 | $name = $cur['name'] ?? ''; 524 | $camel_cased_name = ! empty( $name ) ? self::to_camel_case( $name ) : ''; 525 | $facet = is_string( $camel_cased_name ) && isset( $query[ $camel_cased_name ] ) ? $query[ $camel_cased_name ] : null; 526 | 527 | // Fallback to snakeCased name. 528 | if ( ! isset( $facet ) ) { 529 | $facet = isset( $query[ $name ] ) ? $query[ $name ] : null; 530 | } 531 | 532 | switch ( $cur['type'] ) { 533 | case 'checkboxes': 534 | case 'fselect': 535 | case 'rating': 536 | case 'radio': 537 | case 'dropdown': 538 | case 'hierarchy': 539 | case 'search': 540 | case 'autocomplete': 541 | $prev[ $name ] = $facet; 542 | break; 543 | case 'slider': 544 | case 'date_range': 545 | case 'number_range': 546 | $input = $facet; 547 | $prev[ $name ] = [ 548 | $input['min'] ?? null, 549 | $input['max'] ?? null, 550 | ]; 551 | 552 | break; 553 | case 'proximity': 554 | $input = $facet; 555 | $prev[ $name ] = [ 556 | $input['latitude'] ?? null, 557 | $input['longitude'] ?? null, 558 | $input['chosenRadius'] ?? null, 559 | $input['locationName'] ?? null, 560 | ]; 561 | 562 | break; 563 | 564 | case 'sort': 565 | $sort_options = self::parse_sort_facet_options( $cur ); 566 | 567 | // We pass these through to create our sort args. 568 | $prev[ $name ] = [ 569 | 'is_sort' => true, 570 | 'selected' => $facet, 571 | 'settings' => [ 572 | 'default_label' => $cur['default_label'], 573 | 'sort_options' => $cur['sort_options'], 574 | ], 575 | 'query_args' => [], 576 | ]; 577 | 578 | /** 579 | * Define the query args for the sort. 580 | * 581 | * This is a shim of FacetWP_Facet_Sort::apply_sort() 582 | */ 583 | if ( ! empty( $sort_options[ $facet ] ) ) { 584 | $qa = $sort_options[ $facet ]['query_args']; 585 | 586 | if ( isset( $qa['meta_query'] ) ) { 587 | $prev[ $name ]['query_args']['meta_query'] = $qa['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 588 | } 589 | 590 | $prev[ $name ]['query_args']['orderby'] = $qa['orderby']; 591 | } 592 | 593 | break; 594 | } 595 | 596 | return $prev; 597 | }, 598 | [] 599 | ); 600 | 601 | return $reduced_query ?: []; 602 | } 603 | 604 | /** 605 | * Converts strings to camelCase. 606 | * If an array is supplied, the array keys will be camelCased. 607 | * 608 | * @todo move to helper class. 609 | * 610 | * @param string|array $input The string or list of strings to convert. 611 | * @return string|array The converted string or list of strings. 612 | */ 613 | private static function to_camel_case( $input ) { 614 | if ( is_array( $input ) ) { 615 | $out = []; 616 | 617 | foreach ( $input as $key => $value ) { 618 | $camel_key = self::to_camel_case( $key ); 619 | // @todo refactor recursion to avoid this. 620 | if ( is_string( $camel_key ) ) { 621 | $key = $camel_key; 622 | } 623 | $out[ $key ] = $value; 624 | } 625 | 626 | return $out; 627 | } 628 | 629 | return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) ); 630 | } 631 | 632 | /** 633 | * @todo Work in progress - pull settings from facetwp instead of hard coding them. 634 | * 635 | * phpcs:disable 636 | */ 637 | private static function register_facet_settings() : void { 638 | $facets = FWP()->helper->get_facets(); 639 | $facet_types = FWP()->helper->facet_types; 640 | 641 | // loop over configured facets and loop up facet type. 642 | // call settings_js() if it exists for type. 643 | // determine type for setting field. 644 | // camelCase the field name. 645 | // register graphql object. 646 | 647 | foreach ( $facet_types as $name => $value ) { 648 | if ( method_exists( $facet_types[ $name ], 'settings_js' ) ) { 649 | $settings_name = str_replace( '_', '', ucwords( $name, ' /_' ) ); 650 | // @phpstan-ignore-next-line 651 | $settings = $facet_types[ $name ]->settings_js( [] ); 652 | 653 | foreach ( $settings as $setting ) { 654 | if ( is_array( $setting ) ) { 655 | // recurse. 656 | continue; 657 | } 658 | 659 | $setting_type = gettype( $setting ); 660 | 661 | switch ( $setting_type ) { 662 | case 'string': 663 | $type = 'String'; 664 | break; 665 | } 666 | } 667 | } 668 | } 669 | } 670 | // phpcs:enable 671 | 672 | /** 673 | * Whether to use WPGraphQL Pagination 674 | * 675 | * GraphQL handles pagination differently than the traditional API or FacetWP. 676 | * If you would like to use WP GraphQL's native pagination, filter this value. 677 | * By default, this plugin presumes that anyone using it is deeply familiar with 678 | * FacetWP and is looking for a similar experience using FacetWP with WP GraphQL as 679 | * one would expect its native functionality in WordPress. 680 | * 681 | * If you choose to use the WP GraphQL pagination, then the pager return values 682 | * will not be accurate and you will need to handle the challenge page counts as you 683 | * would for the rest of your GraphQL application. 684 | * 685 | * @see https://graphql.org/learn/pagination/ 686 | */ 687 | public static function use_graphql_pagination(): bool { 688 | return apply_filters( 'wpgraphql_facetwp_user_graphql_pagination', false ); 689 | } 690 | 691 | /** 692 | * Parses the sort options for a sort facet into a WP_Query compatible array. 693 | * 694 | * @see \FacetWP_Facet_Sort::parse_sort_facet() 695 | * 696 | * @param array $facet The facet configuration. 697 | * 698 | * @return array> The parsed sort options. 699 | */ 700 | private static function parse_sort_facet_options( array $facet ): array { 701 | $sort_options = []; 702 | 703 | foreach ( $facet['sort_options'] as $row ) { 704 | $parsed = FWP()->builder->parse_query_obj( [ 'orderby' => $row['orderby'] ] ); 705 | 706 | $sort_options[ $row['name'] ] = [ 707 | 'label' => $row['label'], 708 | 'query_args' => array_intersect_key( 709 | $parsed, 710 | [ 711 | 'meta_query' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 712 | 'orderby' => true, 713 | ] 714 | ), 715 | ]; 716 | } 717 | 718 | $sort_options = apply_filters( 719 | 'facetwp_facet_sort_options', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 720 | $sort_options, 721 | [ 722 | 'facet' => $facet, 723 | 'template_name' => 'graphql', 724 | ] 725 | ); 726 | 727 | return $sort_options; 728 | } 729 | } 730 | -------------------------------------------------------------------------------- /src/Registry/TypeRegistry.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'description' => __( 'Radius of 10', 'wpgraphql-facetwp' ), 39 | 'value' => 10, 40 | ], 41 | WPEnumType::get_safe_name( '25' ) => [ 42 | 'description' => __( 'Radius of 25', 'wpgraphql-facetwp' ), 43 | 'value' => 25, 44 | ], 45 | WPEnumType::get_safe_name( '50' ) => [ 46 | 'description' => __( 'Radius of 50', 'wpgraphql-facetwp' ), 47 | 'value' => 50, 48 | ], 49 | WPEnumType::get_safe_name( '100' ) => [ 50 | 'description' => __( 'Radius of 100', 'wpgraphql-facetwp' ), 51 | 'value' => 100, 52 | ], 53 | WPEnumType::get_safe_name( '250' ) => [ 54 | 'description' => __( 'Radius of 250', 'wpgraphql-facetwp' ), 55 | 'value' => 250, 56 | ], 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Type/Enum/SortOptionsEnum.php: -------------------------------------------------------------------------------- 1 | $facet The facet to register the enum type for. 49 | */ 50 | public static function register_enum( array $facet ): string { 51 | $name = self::get_type_name( $facet['name'] ); 52 | 53 | $sort_options = $facet['sort_options'] ?: []; 54 | 55 | $values = []; 56 | 57 | // Go through the sort facets to generate the values. 58 | foreach ( $sort_options as $option ) { 59 | if ( empty( $option['name'] ) ) { 60 | continue; 61 | } 62 | 63 | $values[ WPEnumType::get_safe_name( $option['name'] ) ] = [ 64 | 'value' => $option['name'], 65 | 'description' => sprintf( 66 | // translators: %s is the label of the sort option. 67 | __( 'Sort by %s', 'wpgraphql-facetwp' ), 68 | $option['label'] 69 | ), 70 | ]; 71 | } 72 | 73 | // Register the enum type. 74 | register_graphql_enum_type( 75 | $name, 76 | [ 77 | 'description' => sprintf( 78 | // translators: %s is the label of the facet. 79 | __( 'Sort options for %s facet.', 'wpgraphql-facetwp' ), 80 | $facet['label'] 81 | ), 82 | 'values' => $values, 83 | ] 84 | ); 85 | 86 | return $name; 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | * 92 | * @param string $name The name of the facet. 93 | */ 94 | public static function get_type_name( string $name ): string { 95 | return 'FacetSortOptions' . graphql_format_type_name( $name ) . 'Enum'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Type/Input/DateRangeArgs.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'String', 38 | 'description' => __( 'The end date', 'wpgraphql-facetwp' ), 39 | ], 40 | 'min' => [ 41 | 'type' => 'String', 42 | 'description' => __( 'The start date', 'wpgraphql-facetwp' ), 43 | ], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/Input/NumberRangeArgs.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'Int', 38 | 'description' => __( 'Maximum value', 'wpgraphql-facetwp' ), 39 | ], 40 | 'min' => [ 41 | 'type' => 'Int', 42 | 'description' => __( 'Minimum value', 'wpgraphql-facetwp' ), 43 | ], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/Input/ProximityArgs.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'type' => ProximityRadiusOptions::get_type_name(), 39 | 'description' => __( 'The chosen radius from the location.', 'wpgraphql-facetwp' ), 40 | ], 41 | 'latitude' => [ 42 | 'type' => 'Float', 43 | 'description' => __( 'The latitude of the location.', 'wpgraphql-facetwp' ), 44 | ], 45 | 'locationName' => [ 46 | 'type' => 'String', 47 | 'description' => __( 'The name of the location.', 'wpgraphql-facetwp' ), 48 | ], 49 | 'longitude' => [ 50 | 'type' => 'Float', 51 | 'description' => __( 'The longitude of the location.', 'wpgraphql-facetwp' ), 52 | ], 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Type/Input/SliderArgs.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'Float', 38 | 'description' => __( 'Maximum value', 'wpgraphql-facetwp' ), 39 | ], 40 | 'min' => [ 41 | 'type' => 'Float', 42 | 'description' => __( 'Minimum value', 'wpgraphql-facetwp' ), 43 | ], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/WPInterface/FacetConfig.php: -------------------------------------------------------------------------------- 1 | [ 41 | 'type' => [ 42 | 'list_of' => FacetChoice::get_type_name(), 43 | ], 44 | 'description' => __( 'Facet choices', 'wpgraphql-facetwp' ), 45 | ], 46 | 'label' => [ 47 | 'type' => 'String', 48 | 'description' => __( 'Facet label.', 'wpgraphql-facetwp' ), 49 | ], 50 | 'name' => [ 51 | 'type' => 'String', 52 | 'description' => __( 'Facet name.', 'wpgraphql-facetwp' ), 53 | ], 54 | 'settings' => [ 55 | 'type' => FacetSettings::get_type_name(), 56 | 'description' => __( 'Facet settings', 'wpgraphql-facetwp' ), 57 | ], 58 | // @todo change to Enum 59 | 'type' => [ 60 | 'type' => 'String', 61 | 'description' => __( 'Facet type', 'wpgraphql-facetwp' ), 62 | ], 63 | ]; 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | * 69 | * @param array $value The value. 70 | */ 71 | public static function get_resolved_type_name( $value ): ?string { 72 | return graphql_format_type_name( $value['type'] ) . 'Facet'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Type/WPObject/Facet.php: -------------------------------------------------------------------------------- 1 | [ 39 | 'type' => [ 40 | 'list_of' => 'String', 41 | ], 42 | 'description' => __( 'Selected values', 'wpgraphql-facetwp' ), 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public static function get_interfaces(): array { 51 | return [ 52 | FacetConfig::get_type_name(), 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Type/WPObject/FacetChoice.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'Int', 38 | 'description' => __( 'Count', 'wpgraphql-facetwp' ), 39 | ], 40 | 'depth' => [ 41 | 'type' => 'Int', 42 | 'description' => __( 'Depth', 'wpgraphql-facetwp' ), 43 | ], 44 | 'label' => [ 45 | 'type' => 'String', 46 | 'description' => __( 'Taxonomy label or post title', 'wpgraphql-facetwp' ), 47 | ], 48 | 'parentId' => [ 49 | 'type' => 'Int', 50 | 'description' => __( 'Parent Term ID (Taxonomy choices only', 'wpgraphql-facetwp' ), 51 | ], 52 | 'termId' => [ 53 | 'type' => 'Int', 54 | 'description' => __( 'Term ID (Taxonomy choices only)', 'wpgraphql-facetwp' ), 55 | ], 56 | 'value' => [ 57 | 'type' => 'String', 58 | 'description' => __( 'Taxonomy value or post ID', 'wpgraphql-facetwp' ), 59 | ], 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Type/WPObject/FacetPager.php: -------------------------------------------------------------------------------- 1 | [ 47 | 'type' => 'Int', 48 | 'description' => __( 'The current page', 'wpgraphql-facetwp' ), 49 | ], 50 | 'per_page' => [ 51 | 'type' => 'Int', 52 | 'description' => __( 'Results per page', 'wpgraphql-facetwp' ), 53 | ], 54 | 'total_rows' => [ 55 | 'type' => 'Int', 56 | 'description' => __( 'Total results', 'wpgraphql-facetwp' ), 57 | ], 58 | 'total_pages' => [ 59 | 'type' => 'Int', 60 | 'description' => __( 'Total pages in results', 'wpgraphql-facetwp' ), 61 | ], 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Type/WPObject/FacetRangeSettings.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'Float', 38 | 'description' => __( 'Slider max value', 'wpgraphql-facetwp' ), 39 | ], 40 | 'min' => [ 41 | 'type' => 'Float', 42 | 'description' => __( 'Slider min value', 'wpgraphql-facetwp' ), 43 | ], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/WPObject/FacetSettings.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'String', 38 | 'description' => __( 'Overflow text', 'wpgraphql-facetwp' ), 39 | ], 40 | 'placeholder' => [ 41 | 'type' => 'String', 42 | 'description' => __( 'Placeholder text', 'wpgraphql-facetwp' ), 43 | ], 44 | 'autoRefresh' => [ 45 | 'type' => 'String', 46 | 'description' => __( 'Auto refresh', 'wpgraphql-facetwp' ), 47 | ], 48 | 'decimalSeparator' => [ 49 | 'type' => 'String', 50 | 'description' => __( 'Decimal separator', 'wpgraphql-facetwp' ), 51 | ], 52 | 'format' => [ 53 | 'type' => 'String', 54 | 'description' => __( 'Date format', 'wpgraphql-facetwp' ), 55 | ], 56 | 'noResultsText' => [ 57 | 'type' => 'String', 58 | 'description' => __( 'No results text', 'wpgraphql-facetwp' ), 59 | ], 60 | 'operator' => [ 61 | 'type' => 'String', 62 | 'description' => __( 'Operator', 'wpgraphql-facetwp' ), 63 | ], 64 | 'prefix' => [ 65 | 'type' => 'String', 66 | 'description' => __( 'Field prefix', 'wpgraphql-facetwp' ), 67 | ], 68 | 'range' => [ 69 | 'type' => FacetRangeSettings::get_type_name(), 70 | 'description' => __( 'Selected slider range values', 'wpgraphql-facetwp' ), 71 | ], 72 | 'searchText' => [ 73 | 'type' => 'String', 74 | 'description' => __( 'Search text', 'wpgraphql-facetwp' ), 75 | ], 76 | 'showExpanded' => [ 77 | 'type' => 'String', 78 | 'description' => __( 'Show expanded facet options', 'wpgraphql-facetwp' ), 79 | ], 80 | 'start' => [ 81 | 'type' => FacetRangeSettings::get_type_name(), 82 | 'description' => __( 'Starting min and max position for the slider', 'wpgraphql-facetwp' ), 83 | ], 84 | 'step' => [ 85 | 'type' => 'Int', 86 | 'description' => __( 'The amount of increase between intervals', 'wpgraphql-facetwp' ), 87 | ], 88 | 'suffix' => [ 89 | 'type' => 'String', 90 | 'description' => __( 'Field suffix', 'wpgraphql-facetwp' ), 91 | ], 92 | 'thousandsSeparator' => [ 93 | 'type' => 'String', 94 | 'description' => __( 'Thousands separator', 'wpgraphql-facetwp' ), 95 | ], 96 | 'defaultLabel' => [ 97 | 'type' => 'String', 98 | 'description' => __( 'Default label', 'wpgraphql-facetwp' ), 99 | ], 100 | 'sortOptions' => [ 101 | 'type' => [ 'list_of' => FacetSortOptionSetting::get_type_name() ], 102 | 'description' => __( 'Sort options', 'wpgraphql-facetwp' ), 103 | ], 104 | ]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Type/WPObject/FacetSortOptionOrderBySetting.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'String', 38 | 'description' => __( 'The orderby key.', 'wpgraphql-facetwp' ), 39 | ], 40 | 'order' => [ 41 | 'type' => 'OrderEnum', 42 | 'description' => __( 'Sort order', 'wpgraphql-facetwp' ), 43 | ], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/WPObject/FacetSortOptionSetting.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'type' => 'String', 38 | 'description' => __( 'Sort option label.', 'wpgraphql-facetwp' ), 39 | ], 40 | 'name' => [ 41 | 'type' => 'String', 42 | 'description' => __( 'Sort option name', 'wpgraphql-facetwp' ), 43 | ], 44 | 'orderby' => [ 45 | 'type' => [ 'list_of' => FacetSortOptionOrderBySetting::get_type_name() ], 46 | 'description' => __( 'The orderby rules for the sort option', 'wpgraphql-facetwp' ), 47 | ], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AxeWP/wp-graphql-facetwp/202a9248a312cd2bb6aaccc17f19cc3d8d36b637/tests/.gitignore -------------------------------------------------------------------------------- /tests/_data/.gitignore: -------------------------------------------------------------------------------- 1 | dump.sql 2 | -------------------------------------------------------------------------------- /tests/_data/_env/docker.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | config: 3 | WPDb: 4 | dsn: "mysql:host=mysql_test;dbname=wpgraphql_facetwp_test" 5 | password: "testing" 6 | WPLoader: 7 | dbHost: "mysql_test" 8 | dbPassword: "testing" 9 | -------------------------------------------------------------------------------- /tests/_data/config.php: -------------------------------------------------------------------------------- 1 | '20', 16 | 'ghosts' => 'no', 17 | 'hierarchical' => 'no', 18 | 'label' => 'Checkboxes', 19 | 'modifier_values' => '', 20 | 'name' => 'checkboxes', 21 | 'operator' => 'and', 22 | 'orderby' => 'count', 23 | 'parent_term' => '', 24 | 'preserve_ghosts' => 'no', 25 | 'show_expanded' => 'no', 26 | 'soft_limit' => 5, 27 | 'source' => 'tax/category', 28 | 'type' => 'checkboxes', 29 | ]; 30 | } 31 | 32 | /** 33 | * Get the default dropdown facet args. 34 | */ 35 | public function get_default_dropdown_facet_args() : array { 36 | return [ 37 | 'count' => '10', 38 | 'hierarchical' => 'no', 39 | 'label' => 'Dropdown', 40 | 'label_any' => 'Any', 41 | 'modifier_type' => 'off', 42 | 'modifier_values' => '', 43 | 'name' => 'dropdown', 44 | 'orderby' => 'count', 45 | 'parent_term' => '', 46 | 'source' => 'post_type', 47 | 'type' => 'dropdown', 48 | ]; 49 | } 50 | 51 | /** 52 | * Get the default radio facet args. 53 | */ 54 | public function get_default_radio_facet_args() : array { 55 | return [ 56 | 'count' => '10', 57 | 'ghosts' => 'no', 58 | 'label_any' => 'Any', 59 | 'label' => 'Radio', 60 | 'modifier_type' => 'off', 61 | 'modifier_values' => '', 62 | 'name' => 'radio', 63 | 'orderby' => 'count', 64 | 'parent_term' => '', 65 | 'preserve_ghosts' => 'no', 66 | 'source' => 'post_type', 67 | 'type' => 'radio', 68 | ]; 69 | } 70 | 71 | /** 72 | * Get the default fselect facet args. 73 | */ 74 | public function get_default_fselect_facet_args() : array { 75 | return [ 76 | 'name' => 'fselect', 77 | 'label' => 'fSelect', 78 | 'type' => 'fselect', 79 | 'source' => 'post_type', 80 | 'label_any' => 'Any', 81 | 'parent_term' => '', 82 | 'modifier_type' => 'off', 83 | 'modifier_values' => '', 84 | 'hierarchical' => 'no', 85 | 'multiple' => 'no', 86 | 'ghosts' => 'no', 87 | 'preserve_ghosts' => 'no', 88 | 'operator' => 'and', 89 | 'orderby' => 'count', 90 | 'count' => '10', 91 | ]; 92 | } 93 | 94 | /** 95 | * Get the default hierarchy facet args. 96 | */ 97 | public function get_default_hierarchy_facet_args() : array { 98 | return [ 99 | 'count' => '10', 100 | 'label_any' => 'Any', 101 | 'label' => 'Hierarchy', 102 | 'modifier_type' => 'off', 103 | 'modifier_values' => '', 104 | 'name' => 'hierarchy', 105 | 'orderby' => 'count', 106 | 'source' => 'post_type', 107 | 'type' => 'hierarchy', 108 | ]; 109 | } 110 | 111 | /** 112 | * Get the default slider facet args. 113 | */ 114 | public function get_default_slider_facet_args() : array { 115 | return [ 116 | 'compare_type' => '', 117 | 'format' => '0,0', 118 | 'label' => 'Slider', 119 | 'name' => 'slider', 120 | 'prefix' => '', 121 | 'reset_text' => 'Reset', 122 | 'source' => 'post_type', 123 | 'step' => '1', 124 | 'suffix' => '', 125 | 'type' => 'slider', 126 | ]; 127 | } 128 | 129 | /** 130 | * Get the default search facet args. 131 | */ 132 | public function get_default_search_facet_args() : array { 133 | return [ 134 | 'auto_refresh' => 'no', 135 | 'label' => 'Search', 136 | 'name' => 'search', 137 | 'placeholder' => '', 138 | 'search_engine' => '', 139 | 'type' => 'search', 140 | ]; 141 | } 142 | 143 | /** 144 | * Get the default autocomplete facet args. 145 | */ 146 | public function get_default_autocomplete_facet_args() : array { 147 | return [ 148 | 'label' => 'Autocomplete', 149 | 'name' => 'autocomplete', 150 | 'placeholder' => '', 151 | 'source' => 'post_type', 152 | 'type' => 'autocomplete', 153 | ]; 154 | } 155 | 156 | /** 157 | * Get the default date_range facet args. 158 | */ 159 | public function get_default_date_range_facet_args() : array { 160 | return [ 161 | 'compare_type' => '', 162 | 'fields' => 'both', 163 | 'format' => '', 164 | 'label' => 'DateRange', 165 | 'name' => 'daterange', 166 | 'source' => 'post_type', 167 | 'type' => 'date_range', 168 | ]; 169 | } 170 | 171 | /** 172 | * Get the default number_range facet args. 173 | */ 174 | public function get_default_number_range_facet_args() : array { 175 | return [ 176 | 'compare_type' => '', 177 | 'fields' => 'both', 178 | 'label' => 'NumberRange', 179 | 'name' => 'numberrange', 180 | 'source_other' => 'post_modified', 181 | 'source' => 'post_type', 182 | 'type' => 'number_range', 183 | ]; 184 | } 185 | 186 | /** 187 | * Get the default rating facet args. 188 | */ 189 | public function get_default_rating_facet_args() : array { 190 | return [ 191 | 'label' => 'StarRating', 192 | 'name' => 'starrating', 193 | 'source' => 'post_type', 194 | 'type' => 'rating', 195 | ]; 196 | } 197 | 198 | /** 199 | * Get the default proximity facet args. 200 | */ 201 | public function get_default_proximity_facet_args() : array { 202 | return [ 203 | 'label' => 'Proximity', 204 | 'name' => 'proximity', 205 | 'radius_default' => '25', 206 | 'radius_max' => '50', 207 | 'radius_min' => '1', 208 | 'radius_options' => '10, 25, 50, 100, 250', 209 | 'radius_ui' => 'dropdown', 210 | 'source' => 'post_type', 211 | 'type' => 'proximity', 212 | 'unit' => 'mi', 213 | ]; 214 | } 215 | 216 | /** 217 | * Get the default pager facet args. 218 | */ 219 | public function get_default_pager_facet_args() : array { 220 | return [ 221 | 'count_text_none' => 'No results', 222 | 'count_text_plural' => '[lower] - [upper] of [total] results', 223 | 'count_text_singular' => '1 result', 224 | 'default_label' => 'Per page', 225 | 'dots_label' => '…', 226 | 'inner_size' => '2', 227 | 'label' => 'Pager_facet', 228 | 'load_more_text' => 'Load more', 229 | 'loading_text' => 'Loading...', 230 | 'name' => 'pager_', 231 | 'next_label' => 'Next »', 232 | 'pager_type' => 'numbers', 233 | 'per_page_options' => '10, 25, 50, 100', 234 | 'prev_label' => '« Prev', 235 | 'type' => 'pager', 236 | ]; 237 | } 238 | 239 | /** 240 | * Get the default reset facet args. 241 | */ 242 | public function get_default_reset_facet_args() : array { 243 | return [ 244 | 'auto_hide' => 'no', 245 | 'label' => 'Reset', 246 | 'name' => 'reset', 247 | 'reset_facets' => [], 248 | 'reset_mode' => 'off', 249 | 'reset_text' => 'Reset', 250 | 'reset_ui' => 'button', 251 | 'type' => 'reset', 252 | ]; 253 | } 254 | 255 | /** 256 | * Get the default sort facet args. 257 | */ 258 | public function get_default_sort_facet_args() : array { 259 | return [ 260 | 'default_label' => 'Sort by', 261 | 'label' => 'Sort_facet', 262 | 'name' => 'sort_facet', 263 | 'type' => 'sort', 264 | 'sort_options' => [], 265 | ]; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/_support/TestCase/FWPGraphQLTestCase.php: -------------------------------------------------------------------------------- 1 | clearFacets(); 25 | $this->clearSchema(); 26 | 27 | unset( FWP()->helper->term_cache ); 28 | } 29 | 30 | /** 31 | * Post test tear down. 32 | */ 33 | public function tearDown(): void { 34 | $this->clearFacets(); 35 | $this->clearSchema(); 36 | 37 | unset( FWP()->helper->term_cache ); 38 | FWP()->indexer->index(); 39 | 40 | // Then... 41 | parent::tearDown(); 42 | } 43 | 44 | public function register_facet( string $facet_type = 'checkbox', array $config = [] ) : array { 45 | $method_name = 'get_default_' . $facet_type . '_facet_args'; 46 | $defaults = $this->tester->$method_name(); 47 | 48 | $config = array_merge( $defaults, $config ); 49 | 50 | FWP()->helper->settings['facets'][] = $config; 51 | 52 | return $config; 53 | } 54 | 55 | public function clearFacets() : void { 56 | FWP()->helper->settings['facets'] = []; 57 | unset( FWP()->facet->facets ); 58 | 59 | // Clear the FacetRegistry::$facets property. 60 | $facets_property = new ReflectionProperty( 'WPGraphQL\FacetWP\Registry\FacetRegistry', 'facets' ); 61 | $facets_property->setAccessible( true ); 62 | $facets_property->setValue( null, null ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/_support/TestCase/FacetTestCase.php: -------------------------------------------------------------------------------- 1 | wantTo( 'activate and deactivate the plugin correctly' ); 9 | 10 | $I->loginAsAdmin(); 11 | $I->amOnPluginsPage(); 12 | $I->seePluginActivated( $pluginSlug ); 13 | $I->deactivatePlugin( $pluginSlug ); 14 | 15 | $I->loginAsAdmin(); 16 | $I->amOnPluginsPage(); 17 | $I->seePluginDeactivated( $pluginSlug ); 18 | $I->activatePlugin( $pluginSlug ); 19 | 20 | $I->loginAsAdmin(); 21 | $I->amOnPluginsPage(); 22 | $I->seePluginActivated( $pluginSlug ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | factory()->term->create( 35 | [ 36 | 'taxonomy' => 'category', 37 | 'name' => 'Category One', 38 | ] 39 | ); 40 | $term_two_id = $this->factory()->term->create( 41 | [ 42 | 'taxonomy' => 'category', 43 | 'name' => 'Category Two', 44 | ] 45 | ); 46 | 47 | $term_one_post_ids = $this->factory()->post->create_many( 48 | 5, 49 | [ 50 | 'post_category' => [ $term_one_id ], 51 | 'post_status' => 'publish', 52 | ] 53 | ); 54 | 55 | $term_two_post_ids = $this->factory()->post->create_many( 56 | 5, 57 | [ 58 | 'post_category' => [ $term_two_id ], 59 | 'post_status' => 'publish', 60 | ] 61 | ); 62 | 63 | $config = array_merge( 64 | $this->tester->get_default_checkbox_facet_args(), 65 | [ 66 | 'name' => 'categories', 67 | 'label' => 'Categories', 68 | 'source' => 'tax/category', 69 | ] 70 | ); 71 | 72 | $this->register_facet( 'checkbox', $config ); 73 | 74 | // Run indexer. 75 | FWP()->indexer->index(); 76 | 77 | // Register facet. 78 | register_graphql_facet_type( 'post' ); 79 | 80 | // Query for fields registered to FacetQueryArgs 81 | $query = ' 82 | query GetFacetQueryArgs{ 83 | __type(name: "FacetQueryArgs") { 84 | inputFields { 85 | name 86 | type { 87 | name 88 | kind 89 | ofType { 90 | name 91 | kind 92 | } 93 | } 94 | } 95 | } 96 | } 97 | '; 98 | 99 | $actual = $this->graphql( compact( 'query' ) ); 100 | 101 | $expected = get_graphql_allowed_facets()[0]; 102 | 103 | $query = ' 104 | query GetPostsByFacet($query: FacetQueryArgs ) { 105 | postFacet(where: {status: PUBLISH, query: $query}) { 106 | facets { 107 | selected 108 | name 109 | label 110 | choices { 111 | value 112 | label 113 | count 114 | } 115 | type 116 | settings { 117 | overflowText 118 | placeholder 119 | autoRefresh 120 | decimalSeparator 121 | format 122 | noResultsText 123 | operator 124 | prefix 125 | range { 126 | min 127 | max 128 | } 129 | searchText 130 | showExpanded 131 | start { 132 | max 133 | min 134 | } 135 | step 136 | suffix 137 | thousandsSeparator 138 | } 139 | } 140 | pager { 141 | page 142 | per_page 143 | total_pages 144 | total_rows 145 | } 146 | posts(first: 10 ) { 147 | pageInfo { 148 | hasNextPage 149 | endCursor 150 | } 151 | nodes { 152 | title 153 | excerpt 154 | categories { 155 | nodes { 156 | databaseId 157 | name 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | '; 165 | 166 | $variables = [ 167 | 'query' => [ 168 | $expected['graphql_field_name'] => [ 'category-one' ], 169 | ], 170 | ]; 171 | 172 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 173 | $this->assertResponseIsValid( $actual ); 174 | $this->assertArrayNotHasKey( 'errors', $actual ); 175 | 176 | // Check that the facet is returned. 177 | $this->assertQuerySuccessful( 178 | $actual, 179 | [ 180 | $this->expectedObject( 181 | 'postFacet', 182 | [ 183 | $this->expectedNode( 184 | 'facets', 185 | [ 186 | $this->expectedField( 'selected', [ 'category-one' ] ), 187 | $this->expectedField( 'name', $expected['name'] ), 188 | $this->expectedField( 'label', $expected['label'] ), 189 | $this->expectedNode( 190 | 'choices', 191 | [ 192 | $this->expectedField( 'value', 'category-one' ), 193 | $this->expectedField( 'label', 'Category One' ), 194 | $this->expectedField( 'count', 5 ), 195 | ] 196 | ), 197 | $this->expectedField( 'type', 'checkboxes' ), 198 | $this->expectedObject( 199 | 'settings', 200 | [ 201 | $this->expectedField( 'showExpanded', $expected['show_expanded'] ), 202 | ] 203 | ), 204 | ], 205 | 0 206 | ), 207 | $this->expectedNode( 208 | 'pager', 209 | [ 210 | $this->expectedField( 'page', 1 ), 211 | $this->expectedField( 'per_page', 10 ), 212 | $this->expectedField( 'total_pages', 1 ), 213 | $this->expectedField( 'total_rows', 5 ), 214 | ] 215 | ), 216 | ] 217 | ), 218 | ] 219 | ); 220 | 221 | // Check that the correct posts are returned. 222 | $this->assertNonEmptyMultidimensionalArray( $actual['data']['postFacet']['posts']['nodes'] ); 223 | $this->assertCount( 5, $actual['data']['postFacet']['posts']['nodes'] ); 224 | $posts_nodes_categories = array_column( $actual['data']['postFacet']['posts']['nodes'], 'categories' ); 225 | 226 | foreach ( $posts_nodes_categories as $node ) { 227 | $this->assertEquals( $term_one_id, $node['nodes'][0]['databaseId'] ); 228 | } 229 | 230 | // Cleanup 231 | wp_delete_term( $term_one_id, 'category' ); 232 | wp_delete_term( $term_two_id, 'category' ); 233 | } 234 | 235 | public function testGetGraphqlAllowedFacets() { 236 | $expected = [ 237 | 'name' => 'test', 238 | 'label' => 'Test', 239 | 'type' => 'checkboxes', 240 | 'source' => 'post_type', 241 | 'post_type' => 'post', 242 | 'choices' => [ 243 | [ 244 | 'value' => 'test', 245 | 'label' => 'Test', 246 | ], 247 | ], 248 | ]; 249 | // Register exposed facet. 250 | $this->register_facet( 'checkbox', $expected ); 251 | // Register unexposed facet. 252 | $this->register_facet( 253 | 'checkbox', 254 | [ 255 | 'name' => 'test2', 256 | 'label' => 'Test2', 257 | 'type' => 'checkboxes', 258 | 'source' => 'post_type', 259 | 'post_type' => 'post', 260 | 'choices' => [ 261 | [ 262 | 'value' => 'test2', 263 | 'label' => 'Test2', 264 | ], 265 | ], 266 | 'show_in_graphql' => false, 267 | ] 268 | ); 269 | // Register facet with explicit properties. 270 | $this->register_facet( 271 | 'checkbox', 272 | [ 273 | 'name' => 'test3', 274 | 'label' => 'Test3', 275 | 'type' => 'checkboxes', 276 | 'source' => 'post_type', 277 | 'post_type' => 'post', 278 | 'show_in_graphql' => true, 279 | 'graphql_field_name' => 'myTestField', 280 | ] 281 | ); 282 | 283 | $allowed_facets = get_graphql_allowed_facets(); 284 | 285 | codecept_debug( $allowed_facets ); 286 | 287 | // Check only one facet is returned. 288 | $this->assertEquals( 2, count( $allowed_facets ), 'Only exposed facet should be returned' ); 289 | 290 | // Check that the first the correct facet. and the default properties are set. 291 | $this->assertEquals( $expected['name'], $allowed_facets[0]['name'], 'The first facet should be returned' ); 292 | $this->assertTrue( $allowed_facets[0]['show_in_graphql'] ); 293 | $this->assertEquals( graphql_format_field_name( $expected['name'] ), $allowed_facets[0]['graphql_field_name'], 'The GraphQL field name should be set by default based on the name' ); 294 | $this->assertEquals( [ 'list_of' => 'String' ], $allowed_facets[0]['graphql_type'], 'A checkbox facet should return a String[]' ); 295 | 296 | // Test that the GraphQL properties can be overridden. 297 | $this->assertEquals( 'test3', $allowed_facets[1]['name'], 'The last facet should be returned' ); 298 | $this->assertTrue( $allowed_facets[1]['show_in_graphql'] ); 299 | $this->assertEquals( 'myTestField', $allowed_facets[1]['graphql_field_name'], 'The GraphQL field name should be overridden' ); 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /tests/wpunit/MainTest.php: -------------------------------------------------------------------------------- 1 | getProperty( 'instance' ); 20 | $property->setAccessible( true ); 21 | $property->setValue( null ); 22 | } 23 | 24 | public function tearDown(): void { 25 | // Your tear down methods here. 26 | 27 | unset( $this->instance ); 28 | \WPGraphQL::clear_schema(); 29 | 30 | // Then... 31 | parent::tearDown(); 32 | } 33 | 34 | // Tests 35 | /** 36 | * Test instance 37 | * 38 | * @covers \WPGraphQL\FacetWP\Main 39 | */ 40 | public function testInstance() { 41 | $this->instance = new Main(); 42 | 43 | $this->assertTrue( $this->instance instanceof Main ); 44 | } 45 | /** 46 | * Test instance 47 | * 48 | * @covers \WPGraphQL\FacetWP\Main 49 | */ 50 | public function testInstanceBeforeInstantiation() { 51 | $instances = Main::instance(); 52 | $this->assertNotEmpty( $instances ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/wpunit/SortFacetTest.php: -------------------------------------------------------------------------------- 1 | post_ids = $this->generate_posts( 10 ); 21 | } 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | public function tearDown(): void { 27 | // cleanup. 28 | foreach ( $this->post_ids as $id ) { 29 | wp_delete_post( $id, true ); 30 | } 31 | 32 | parent::tearDown(); 33 | } 34 | 35 | protected function get_facet_config( array $overrides = [] ) : array { 36 | $default_config = $this->tester->get_default_sort_facet_args(); 37 | 38 | return array_merge( $default_config, $overrides ); 39 | } 40 | 41 | protected function get_sort_option_orderby( string $type, string $order = 'DESC' ) : array { 42 | $possible_configs = [ 43 | 'date' => [ 44 | 'key' => 'date', 45 | 'order' => $order, 46 | 'type' => 'CHAR', 47 | ], 48 | 'ID' => [ 49 | 'key' => 'ID', 50 | 'order' => $order, 51 | 'type' => 'CHAR', 52 | ], 53 | 'name' => [ 54 | 'key' => 'name', 55 | 'order' => $order, 56 | 'type' => 'CHAR', 57 | ], 58 | 'title' => [ 59 | 'key' => 'title', 60 | 'order' => $order, 61 | 'type' => 'CHAR', 62 | ], 63 | 'type' => [ 64 | 'key' => 'type', 65 | 'order' => $order, 66 | 'type' => 'CHAR', 67 | ], 68 | 'modified' => [ 69 | 'key' => 'modified', 70 | 'order' => $order, 71 | 'type' => 'CHAR', 72 | ], 73 | 'comment_count' => [ 74 | 'key' => 'comment_count', 75 | 'order' => $order, 76 | 'type' => 'CHAR', 77 | ], 78 | 'menu_order' => [ 79 | 'key' => 'menu_order', 80 | 'order' => $order, 81 | 'type' => 'CHAR', 82 | ], 83 | 'post__in' => [ 84 | 'key' => 'post__in', 85 | 'order' => $order, 86 | 'type' => 'CHAR', 87 | ], 88 | 'test_meta' => [ 89 | 'key' => 'cf/test_meta', 90 | 'order' => $order, 91 | 'type' => 'CHAR', 92 | ], 93 | ]; 94 | 95 | return $possible_configs[ $type ]; 96 | } 97 | 98 | protected function generate_posts( int $amt, array $default_args = [] ) : array { 99 | $results = []; 100 | $default_args = array_merge( 101 | [ 102 | 'post_status' => 'publish', 103 | 'post_type' => 'page', 104 | ], 105 | $default_args 106 | ); 107 | 108 | for ( $i = 0; $i < $amt; $i++ ) { 109 | // Stagger the date created and modified times. 110 | $default_args['post_date'] = date( 'Y-m-d H:i:s', strtotime( "-$i days" ) ); 111 | $default_args['post_modified'] = date( 'Y-m-d H:i:s', strtotime( "-$i days" ) ); 112 | // Set a menu order. 113 | $default_args['menu_order'] = $i; 114 | 115 | $results[] = $this->factory()->post->create( $default_args ); 116 | } 117 | 118 | return $results; 119 | } 120 | 121 | protected function get_query() : string { 122 | return ' 123 | query SortFacetQuery( $query: FacetQueryArgs ){ 124 | pageFacet( where: { query: $query } ) { 125 | facets { 126 | label 127 | name 128 | type 129 | selected 130 | settings { 131 | defaultLabel 132 | sortOptions { 133 | label 134 | name 135 | orderby { 136 | key 137 | order 138 | } 139 | } 140 | } 141 | } 142 | pages { 143 | nodes { 144 | databaseId 145 | date 146 | modified 147 | slug 148 | title 149 | menuOrder 150 | } 151 | } 152 | } 153 | } 154 | '; 155 | } 156 | 157 | protected function assertValidSort( array $actual, string $key, string $direction ) : void { 158 | for ( $i = 0; $i < count( $actual ); $i++ ) { 159 | // Bail if we're at the end of the array. 160 | if ( ! isset( $actual[ $i + 1 ] ) ) { 161 | break; 162 | } 163 | 164 | if ( 'ASC' === $direction ) { 165 | $this->assertLessThanOrEqual( $actual[ $i + 1 ][ $key ], $actual[ $i ][ $key ], $key . ' is not in ascending order.' ); 166 | } else { 167 | $this->assertGreaterThanOrEqual( $actual[ $i + 1 ][ $key ], $actual[ $i ][ $key ], $key . ' is not in descending order.' ); 168 | } 169 | } 170 | } 171 | 172 | public function testSortFacet() : void { 173 | $sort_types = [ 174 | 'date' => 'date', 175 | 'ID' => 'databaseId', 176 | 'name' => 'slug', 177 | 'title' => 'title', 178 | 'modified' => 'modified', 179 | 'menu_order' => 'menuOrder', 180 | ]; 181 | 182 | $sort_options = []; 183 | 184 | foreach ( array_keys( $sort_types ) as $key ) { 185 | $sort_options[] = [ 186 | 'label' => $key . ' (ASC)', 187 | 'name' => $key . '_asc', 188 | 'orderby' => [ 189 | $this->get_sort_option_orderby( $key, 'ASC' ), 190 | ], 191 | ]; 192 | $sort_options[] = [ 193 | 'label' => $key . ' (DESC)', 194 | 'name' => $key . '_desc', 195 | 'orderby' => [ 196 | $this->get_sort_option_orderby( $key, 'DESC' ), 197 | ], 198 | ]; 199 | } 200 | 201 | $facet_config = $this->get_facet_config( 202 | [ 203 | 'name' => 'post_sort', 204 | 'label' => 'Post sort', 205 | 'sort_options' => $sort_options, 206 | ] 207 | ); 208 | 209 | $this->register_facet( 'sort', $facet_config ); 210 | FWP()->indexer->index(); 211 | 212 | // Register facet. 213 | register_graphql_facet_type( 'page' ); 214 | 215 | $query = $this->get_query(); 216 | 217 | $variables = [ 218 | 'query' => [ 219 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][0]['name'] ), 220 | ], 221 | ]; 222 | 223 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 224 | 225 | $this->assertArrayNotHasKey( 'errors', $actual ); 226 | $this->assertQuerySuccessful( 227 | $actual, 228 | [ 229 | // Test Facet. 230 | $this->expectedNode( 231 | 'pageFacet.facets', 232 | [ 233 | $this->expectedField( 'name', $facet_config['name'] ), 234 | $this->expectedField( 'label', $facet_config['label'] ), 235 | $this->expectedField( 'type', $facet_config['type'] ), 236 | $this->expectedObject( 237 | 'settings', 238 | [ 239 | $this->expectedField( 'defaultLabel', $facet_config['default_label'] ), 240 | $this->expectedNode( 241 | 'sortOptions', 242 | [ 243 | $this->expectedField( 'label', $facet_config['sort_options'][0]['label'] ), 244 | $this->expectedField( 'name', $facet_config['sort_options'][0]['name'] ), 245 | $this->expectedNode( 246 | 'orderby', 247 | [ 248 | $this->expectedField( 'key', $facet_config['sort_options'][0]['orderby'][0]['key'] ), 249 | $this->expectedField( 'order', WPEnumType::get_safe_name( $facet_config['sort_options'][0]['orderby'][0]['order'] ) ), 250 | ], 251 | 0 252 | ), 253 | ], 254 | 0 255 | ), 256 | ], 257 | ), 258 | ], 259 | 0 260 | ), 261 | ] 262 | ); 263 | // Test the cont of the sort options. 264 | $this->assertCount( count( $facet_config['sort_options'] ), $actual['data']['pageFacet']['facets'][0]['settings']['sortOptions'] ); 265 | 266 | // Test the post sorting. 267 | foreach ( $sort_options as $option ) { 268 | $variables = [ 269 | 'query' => [ 270 | $facet_config['name'] => WPEnumType::get_safe_name( $option['name'] ), 271 | ], 272 | ]; 273 | 274 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 275 | 276 | $this->assertArrayNotHasKey( 'errors', $actual ); 277 | 278 | $posts = $actual['data']['pageFacet']['pages']['nodes']; 279 | 280 | $this->assertCount( count( $this->post_ids ), $posts ); 281 | 282 | $actual_post_ids = wp_list_pluck( $posts, 'databaseId' ); 283 | sort( $actual_post_ids ); 284 | $this->assertEqualSets( $this->post_ids, $actual_post_ids ); 285 | 286 | $this->assertValidSort( $posts, $sort_types[ $option['orderby'][0]['key'] ], $option['orderby'][0]['order'] ); 287 | } 288 | } 289 | 290 | public function testMultiSortFacetByCommentCount() : void { 291 | $comment_id_1 = $this->factory->comment->create( [ 'comment_post_ID' => $this->post_ids[3] ] ); 292 | $comment_id_2 = $this->factory->comment->create( [ 'comment_post_ID' => $this->post_ids[3] ] ); 293 | $comment_id_3 = $this->factory->comment->create( [ 'comment_post_ID' => $this->post_ids[7] ] ); 294 | 295 | $sort_options = [ 296 | [ 297 | 'label' => 'Comment count (ASC)', 298 | 'name' => 'comment_count_asc', 299 | 'orderby' => [ 300 | $this->get_sort_option_orderby( 'comment_count', 'ASC' ), 301 | $this->get_sort_option_orderby( 'date', 'DESC' ), 302 | ], 303 | ], 304 | [ 305 | 'label' => 'Comment count (DESC)', 306 | 'name' => 'comment_count_desc', 307 | 'orderby' => [ 308 | $this->get_sort_option_orderby( 'comment_count', 'DESC' ), 309 | $this->get_sort_option_orderby( 'date', 'ASC' ), 310 | ], 311 | ], 312 | ]; 313 | 314 | $facet_config = $this->get_facet_config( 315 | [ 316 | 'name' => 'comment_sort', 317 | 'label' => 'Comment multisort', 318 | 'sort_options' => $sort_options, 319 | ] 320 | ); 321 | 322 | $this->register_facet( 'sort', $facet_config ); 323 | FWP()->indexer->index(); 324 | 325 | $this->clearSchema(); 326 | 327 | register_graphql_facet_type( 'page' ); 328 | 329 | $query = $this->get_query(); 330 | 331 | // Test ascending. 332 | $variables = [ 333 | 'query' => [ 334 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][0]['name'] ), 335 | ], 336 | ]; 337 | 338 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 339 | 340 | $this->assertArrayNotHasKey( 'errors', $actual ); 341 | 342 | $posts = $actual['data']['pageFacet']['pages']['nodes']; 343 | 344 | // Test most comment post is last. 345 | $this->assertEquals( $this->post_ids[3], $posts[ count( $posts ) - 1 ]['databaseId'] ); 346 | // Test second most comment post is second to last. 347 | $this->assertEquals( $this->post_ids[7], $posts[ count( $posts ) - 2 ]['databaseId'] ); 348 | 349 | // Test the rest of the posts are in the correct order. 350 | $this->assertValidSort( array_slice( $posts, 0, count( $posts ) - 2 ), 'date', 'DESC' ); 351 | 352 | // Test descending. 353 | $variables = [ 354 | 'query' => [ 355 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][1]['name'] ), 356 | ], 357 | ]; 358 | 359 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 360 | 361 | $this->assertArrayNotHasKey( 'errors', $actual ); 362 | 363 | $posts = $actual['data']['pageFacet']['pages']['nodes']; 364 | 365 | // Test most comment post is first. 366 | $this->assertEquals( $this->post_ids[3], $posts[0]['databaseId'] ); 367 | // Test second most comment post is second. 368 | $this->assertEquals( $this->post_ids[7], $posts[1]['databaseId'] ); 369 | 370 | // Test the rest of the posts are in the correct order. 371 | $this->assertValidSort( array_slice( $posts, 2 ), 'date', 'ASC' ); 372 | 373 | // Cleanup. 374 | wp_delete_comment( $comment_id_1, true ); 375 | wp_delete_comment( $comment_id_2, true ); 376 | wp_delete_comment( $comment_id_3, true ); 377 | } 378 | 379 | public function testMultiSortFacetByCustomField() : void { 380 | // Update the post meta. 381 | update_post_meta( $this->post_ids[3], 'test_meta', 'a' ); 382 | update_post_meta( $this->post_ids[7], 'test_meta', 'b' ); 383 | 384 | $sort_options = [ 385 | [ 386 | 'label' => 'Test meta (ASC)', 387 | 'name' => 'test_meta_asc', 388 | 'orderby' => [ 389 | $this->get_sort_option_orderby( 'test_meta', 'ASC' ), 390 | $this->get_sort_option_orderby( 'date', 'DESC' ), 391 | ], 392 | ], 393 | [ 394 | 'label' => 'Test meta (DESC)', 395 | 'name' => 'test_meta_desc', 396 | 'orderby' => [ 397 | $this->get_sort_option_orderby( 'test_meta', 'DESC' ), 398 | $this->get_sort_option_orderby( 'date', 'ASC' ), 399 | ], 400 | ], 401 | ]; 402 | 403 | $facet_config = $this->get_facet_config( 404 | [ 405 | 'name' => 'test_meta_sort', 406 | 'label' => 'Test meta multisort', 407 | 'sort_options' => $sort_options, 408 | ] 409 | ); 410 | 411 | $this->register_facet( 'sort', $facet_config ); 412 | FWP()->indexer->index(); 413 | 414 | $this->clearSchema(); 415 | 416 | register_graphql_facet_type( 'page' ); 417 | 418 | $query = $this->get_query(); 419 | 420 | // Test ascending. 421 | $variables = [ 422 | 'query' => [ 423 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][0]['name'] ), 424 | ], 425 | ]; 426 | 427 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 428 | 429 | $this->assertArrayNotHasKey( 'errors', $actual ); 430 | 431 | $posts = $actual['data']['pageFacet']['pages']['nodes']; 432 | 433 | // Check ascending order. 434 | $first_post_meta = get_post_meta( $posts[0]['databaseId'], 'test_meta', true ); 435 | $second_post_meta = get_post_meta( $posts[1]['databaseId'], 'test_meta', true ); 436 | 437 | $this->assertLessThanOrEqual( $second_post_meta, $first_post_meta ); 438 | 439 | // Test descending. 440 | $variables = [ 441 | 'query' => [ 442 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][1]['name'] ), 443 | ], 444 | ]; 445 | 446 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 447 | 448 | $this->assertArrayNotHasKey( 'errors', $actual ); 449 | 450 | $posts = $actual['data']['pageFacet']['pages']['nodes']; 451 | 452 | $first_post_meta = get_post_meta( $posts[0]['databaseId'], 'test_meta', true ); 453 | $second_post_meta = get_post_meta( $posts[1]['databaseId'], 'test_meta', true ); 454 | $this->assertGreaterThanOrEqual( $second_post_meta, $first_post_meta ); 455 | } 456 | 457 | public function testSortFacetWithFacet() : void { 458 | // Create test data. 459 | $term_one_id = $this->factory()->term->create( 460 | [ 461 | 'taxonomy' => 'category', 462 | 'name' => 'Term one', 463 | ] 464 | ); 465 | 466 | $term_two_id = $this->factory()->term->create( 467 | [ 468 | 'taxonomy' => 'category', 469 | 'name' => 'Term two', 470 | ] 471 | ); 472 | 473 | $term_one_post_ids = $this->generate_posts( 474 | 3, 475 | [ 476 | 'post_type' => 'post', 477 | 'post_category' => [ $term_one_id ], 478 | ] 479 | ); 480 | 481 | $term_two_post_ids = $this->generate_posts( 482 | 3, 483 | [ 484 | 'post_type' => 'post', 485 | 'post_category' => [ $term_two_id ], 486 | ] 487 | ); 488 | 489 | // Checkbox Facet 490 | $checkbox_config = array_merge( 491 | $this->tester->get_default_checkbox_facet_args(), 492 | [ 493 | 'name' => 'categories', 494 | 'label' => 'Categories', 495 | 'source' => 'tax/category', 496 | ] 497 | ); 498 | 499 | $this->register_facet( 'checkbox', $checkbox_config ); 500 | 501 | $sort_options = [ 502 | [ 503 | 'label' => 'Title (ASC)', 504 | 'name' => 'title_asc', 505 | 'orderby' => [ 506 | $this->get_sort_option_orderby( 'title', 'ASC' ), 507 | ], 508 | ], 509 | [ 510 | 'label' => 'Title (DESC)', 511 | 'name' => 'title_desc', 512 | 'orderby' => [ 513 | $this->get_sort_option_orderby( 'title', 'DESC' ), 514 | ], 515 | ], 516 | ]; 517 | 518 | $facet_config = $this->get_facet_config( 519 | [ 520 | 'name' => 'title_sort', 521 | 'label' => 'Title multisort', 522 | 'sort_options' => $sort_options, 523 | ] 524 | ); 525 | 526 | $this->register_facet( 'sort', $facet_config ); 527 | FWP()->indexer->index(); 528 | 529 | register_graphql_facet_type( 'post' ); 530 | 531 | $query = ' 532 | query GetSortCheckboxFacetPosts($query: FacetQueryArgs ) { 533 | postFacet( where: {query: $query} ) { 534 | posts { 535 | nodes { 536 | databaseId 537 | title 538 | categories { 539 | nodes { 540 | databaseId 541 | name 542 | } 543 | } 544 | } 545 | } 546 | } 547 | } 548 | '; 549 | 550 | // Test ascending 551 | $variables = [ 552 | 'query' => [ 553 | $checkbox_config['name'] => 'term-one', 554 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][0]['name'] ), 555 | ], 556 | ]; 557 | 558 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 559 | 560 | $this->assertArrayNotHasKey( 'errors', $actual ); 561 | 562 | $posts = $actual['data']['postFacet']['posts']['nodes']; 563 | 564 | foreach ( $posts as $posts ) { 565 | $this->assertContains( $term_one_id, wp_list_pluck( $posts['categories']['nodes'], 'databaseId' ) ); 566 | } 567 | 568 | $this->assertValidSort( $posts, 'title', 'ASC' ); 569 | 570 | // Test descending. 571 | $variables = [ 572 | 'query' => [ 573 | $checkbox_config['name'] => 'term-one', 574 | $facet_config['name'] => WPEnumType::get_safe_name( $facet_config['sort_options'][1]['name'] ), 575 | ], 576 | ]; 577 | 578 | $actual = $this->graphql( compact( 'query', 'variables' ) ); 579 | 580 | $this->assertArrayNotHasKey( 'errors', $actual ); 581 | 582 | $posts = $actual['data']['postFacet']['posts']['nodes']; 583 | 584 | foreach ( $posts as $posts ) { 585 | $this->assertContains( $term_one_id, wp_list_pluck( $posts['categories']['nodes'], 'databaseId' ) ); 586 | } 587 | 588 | $this->assertValidSort( $posts, 'title', 'DESC' ); 589 | } 590 | 591 | public function testMultiSortFacetQueryWithPostType() : void { 592 | $this->markTestIncomplete( 'Facets are currently limited to a single post type.' ); 593 | } 594 | 595 | public function testMultiSortFacetQueryWithPostIn() : void { 596 | $this->markTestIncomplete( 'Need to test the Proximity facet, or some other facet that relies on the sort facet.' ); 597 | } 598 | 599 | } 600 | -------------------------------------------------------------------------------- /wp-graphql-facetwp.php: -------------------------------------------------------------------------------- 1 | The list of missing dependencies. 79 | */ 80 | function dependencies_not_ready(): array { 81 | $wpgraphql_version = '1.6.0'; 82 | $facetwp_version = '4.0'; 83 | 84 | $deps = []; 85 | 86 | if ( ! class_exists( 'WPGraphQL' ) || ( defined( 'WPGRAPHQL_VERSION' ) && version_compare( WPGRAPHQL_VERSION, $wpgraphql_version, '<' ) ) ) { 87 | $deps['WPGraphQL'] = $wpgraphql_version; 88 | } 89 | 90 | if ( ! class_exists( 'FacetWP' ) || ( defined( 'FACETWP_VERSION' ) && version_compare( FACETWP_VERSION, $facetwp_version, '<' ) ) ) { 91 | $deps['FacetWP'] = $facetwp_version; 92 | } 93 | 94 | return $deps; 95 | } 96 | 97 | /** 98 | * Initializes the plugin. 99 | */ 100 | function init(): void { 101 | constants(); 102 | 103 | $not_ready = dependencies_not_ready(); 104 | 105 | if ( empty( $not_ready ) && defined( 'WPGRAPHQL_FACETWP_PLUGIN_DIR' ) ) { 106 | require_once WPGRAPHQL_FACETWP_PLUGIN_DIR . 'src/Main.php'; 107 | \WPGraphQL\FacetWP\Main::instance(); 108 | 109 | return; 110 | } 111 | 112 | /** 113 | * For users with lower capabilities, don't show the notice. 114 | * 115 | * @todo Are we sure we don't want to tell all users with backend access that the plugin isn't working? 116 | */ 117 | if ( ! current_user_can( 'manage_options' ) ) { 118 | return; 119 | } 120 | 121 | foreach ( $not_ready as $dep => $version ) { 122 | add_action( 123 | 'admin_notices', 124 | static function () use ( $dep, $version ) { 125 | ?> 126 |
127 |

128 | 136 |

137 |
138 | 139 |