├── .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 | 
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 |     
11 | 
12 | 
13 | 
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 |
138 |
139 |