├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── release.yml └── workflows │ ├── styles.yml │ └── tests.yml ├── .gitignore ├── .php-cs-fixer.php ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── composer.json ├── config └── scripts │ └── install-wp-tests.sh ├── phpstan.neon ├── phpunit-wp.xml ├── phpunit.xml ├── rector.php ├── renovate.json ├── src ├── Api │ ├── CustomModelTypeInterface.php │ └── WithMetaModelInterface.php ├── Builders │ ├── AbstractBuilder.php │ ├── AbstractWithMetaBuilder.php │ ├── CommentBuilder.php │ ├── OptionBuilder.php │ ├── PostBuilder.php │ ├── TermBuilder.php │ └── UserBuilder.php ├── Concerns │ └── HasMetas.php ├── Enums │ ├── PingStatus.php │ ├── PostStatus.php │ └── YesNo.php ├── Exceptions │ ├── CannotOverrideCustomTypeException.php │ ├── MetaNotSupportedException.php │ ├── NotAllowedException.php │ └── WpOrmException.php ├── MetaMappingConfig.php ├── Models │ ├── Article.php │ ├── Attachment.php │ ├── Comment.php │ ├── CustomComment.php │ ├── CustomPost.php │ ├── Meta │ │ ├── AbstractMeta.php │ │ ├── MetaInterface.php │ │ ├── PostMeta.php │ │ └── UserMeta.php │ ├── Multisite │ │ ├── Blog.php │ │ ├── BlogVersion.php │ │ ├── RegistrationLog.php │ │ ├── Signup.php │ │ ├── Site.php │ │ └── SiteMeta.php │ ├── Option.php │ ├── Page.php │ ├── Post.php │ ├── Term.php │ ├── TermRelationship.php │ ├── TermTaxonomy.php │ └── User.php ├── Orm │ ├── AbstractModel.php │ ├── Builder.php │ ├── Database.php │ ├── Query │ │ ├── Grammars │ │ │ └── WordPressGrammar.php │ │ └── Processors │ │ │ └── WordPressProcessor.php │ ├── Resolver.php │ └── Schemas │ │ └── WordPressBuilder.php ├── Scopes │ └── CustomModelTypeScope.php ├── Taps │ ├── Attachment │ │ └── IsMimeTypeTap.php │ ├── Comment │ │ ├── IsApprovedTap.php │ │ ├── IsCommentTypeTap.php │ │ └── IsUserTap.php │ ├── Option │ │ └── IsAutoloadTap.php │ └── Post │ │ ├── IsAuthorTap.php │ │ ├── IsPingStatusTap.php │ │ ├── IsPostTypeTap.php │ │ └── IsStatusTap.php └── helpers.php ├── tests ├── Unit │ ├── Bootstrap.php │ ├── Concerns │ │ └── HasMetasTest.php │ ├── Models │ │ ├── CommentTest.php │ │ ├── CustomCommentTest.php │ │ └── CustomPostTest.php │ ├── Orm │ │ └── DatabaseTest.php │ └── Scopes │ │ └── CustomModelTypeScopeTest.php └── WordPress │ ├── Bootstrap.php │ ├── Concerns │ ├── HasMetasTest.php │ └── PrunableTest.php │ ├── Models │ ├── CommentTest.php │ ├── CustomCommentTest.php │ ├── CustomPostTest.php │ ├── OptionTest.php │ ├── PostTest.php │ └── UserTest.php │ ├── Orm │ ├── AbstractModelTest.php │ ├── AbstractModelWithCustomTableTest.php │ ├── DatabaseTest.php │ ├── DatabaseTransactionTest.php │ └── Schemas │ │ └── WordPressBuilderTest.php │ └── TestCase.php └── wp-tests-config-sample.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG]: ' 5 | labels: ["bug"] 6 | assignees: 7 | - dimitriBouteille 8 | 9 | --- 10 | 11 | ### Describe the bug 12 | 13 | A clear and concise description of what the bug is. 14 | 15 | ### Steps to reproduce the issue 16 | 17 | A clear and concise description to reproduce the bug. 18 | 19 | ### Expected behavior 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | ### Your setup 24 | 25 | - WordPress Version: [e.g. 6.4.2] 26 | - Module version: [e.g. 3.0.0] 27 | - Are you using framework ?: [e.g. Bedrock, Wordplate, ... - If yes, Please specify the framework and version] 28 | 29 | ### Additional context 30 | 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE]: ' 5 | labels: ["enhancement"] 6 | assignees: 7 | - dimitriBouteille 8 | 9 | --- 10 | 11 | ### Introduction 12 | 13 | Making your own contribution is easy, encouraged and greatly appreciated! We will put effort into reviewing and merging your PR quickly. For more info, please refer to the [contribution guidelines](https://github.com/dbout/wp-orm/blob/develop/CONTRIBUTING.md). 14 | 15 | **Is your feature request related to a problem? Please describe.** 16 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 17 | 18 | ### Describe the solution you'd like 19 | 20 | A clear and concise description of what you want to happen. 21 | 22 | ### Describe alternatives you've considered 23 | 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | 26 | ### Additional context 27 | 28 | Add any other context or screenshots about the feature request here. 29 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Breaking Changes 🛠 4 | labels: 5 | - Breaking change 6 | - title: New Features 💎 7 | labels: 8 | - Feature 9 | - title: Fixes ⛑️ 10 | labels: 11 | - Fix 12 | - title: Other Changes 🖇️ 13 | labels: 14 | - "*" -------------------------------------------------------------------------------- /.github/workflows/styles.yml: -------------------------------------------------------------------------------- 1 | name: Coding Styles 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | codingStyle: 11 | name: "Coding Style" 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: "Checkout repo" 16 | uses: actions/checkout@v4 17 | 18 | - name: "Configure PHP" 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: 8.2 22 | tools: composer:v2 23 | 24 | - name: "Install dependencies" 25 | run: composer install --prefer-dist --no-progress 26 | 27 | - name: "PHPCsFixer" 28 | run: composer csFixer 29 | 30 | - name: Rector 31 | run: composer rector 32 | 33 | - name: "PHPStan Analyse" 34 | run: composer phpstan 35 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | unit-test: 11 | name: "Unit tests" 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: "Checkout repo" 16 | uses: actions/checkout@v4 17 | 18 | - name: "Install PHP" 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: "8.2" 22 | tools: composer:v2 23 | coverage: xdebug 24 | 25 | - name: "Install composer dependencies" 26 | run: composer install --prefer-dist --no-progress 27 | 28 | - name: "Run unit tests with coverage" 29 | run: composer run test:unit:coverage 30 | 31 | - name: "Send coverage to Coveralls" 32 | env: 33 | COVERALLS_REPO_TOKEN: ${{ github.token }} 34 | if: ${{ env.COVERALLS_REPO_TOKEN }} 35 | uses: coverallsapp/github-action@v2 36 | with: 37 | github-token: ${{ env.COVERALLS_REPO_TOKEN }} 38 | flag-name: "unit" 39 | allow-empty: false 40 | parallel: true 41 | 42 | wp-test: 43 | name: "WordPress tests with WP ${{ matrix.wp_version }}" 44 | runs-on: ubuntu-latest 45 | 46 | strategy: 47 | matrix: 48 | wp_version: ["6.3", "6.4", "6.5", "6.6", "6.7", "6.8", "latest"] 49 | 50 | services: 51 | mysql: 52 | image: mysql:9.4 53 | env: 54 | MYSQL_ALLOW_EMPTY_PASSWORD: false 55 | ports: 56 | - 3306:3306 57 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 58 | 59 | steps: 60 | - name: "Install subversion" 61 | run: sudo apt-get install -y subversion 62 | 63 | - name: "Checkout repo" 64 | uses: actions/checkout@v4 65 | 66 | - name: "Install PHP" 67 | uses: shivammathur/setup-php@v2 68 | with: 69 | php-version: "8.2" 70 | tools: composer:v2 71 | coverage: xdebug 72 | 73 | - name: "Install composer dependencies" 74 | run: composer install --prefer-dist --no-progress 75 | 76 | # WordPress tests works only with PHPUnit 9.x :( 77 | # https://make.wordpress.org/core/handbook/references/phpunit-compatibility-and-wordpress-versions/ 78 | - name: "Install PHPUnit v9" 79 | run: | 80 | composer require --dev --update-with-all-dependencies 'phpunit/phpunit:^9.0' 81 | composer require --dev --update-with-all-dependencies 'yoast/phpunit-polyfills:^3.0' 82 | 83 | - name: "Install WP" 84 | shell: bash 85 | run: ./config/scripts/install-wp-tests.sh wordpress_test root '' 127.0.0.1:3306 ${{ matrix.wp_version }} 86 | 87 | - name: "Run WordPress tests with coverage" 88 | run: composer run test:wordPress:coverage 89 | 90 | - name: "Send coverage to Coveralls" 91 | env: 92 | COVERALLS_REPO_TOKEN: ${{ github.token }} 93 | if: ${{ env.COVERALLS_REPO_TOKEN }} 94 | uses: coverallsapp/github-action@v2 95 | with: 96 | github-token: ${{ env.COVERALLS_REPO_TOKEN }} 97 | flag-name: wp-test-$ 98 | allow-empty: false 99 | parallel: true 100 | 101 | finish: 102 | needs: 103 | - unit-test 104 | - wp-test 105 | if: ${{ always() }} 106 | runs-on: ubuntu-latest 107 | steps: 108 | - name: Close parallel build 109 | uses: coverallsapp/github-action@v2 110 | with: 111 | parallel-finished: true 112 | carryforward: "wp-test-1,wp-test-2,wp-test-3,wp-test-4,wp-test-5,wp-test-6,unit" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | var/ 4 | .idea/ 5 | composer.lock 6 | .php-cs-fixer.cache 7 | web 8 | /package-lock.json 9 | build/ 10 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | $finder = \PhpCsFixer\Finder::create() 10 | ->name('*.php') 11 | ->in([ 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]) 15 | ; 16 | 17 | $header = << 22 | EOF; 23 | 24 | $config = new \PhpCsFixer\Config(); 25 | return $config 26 | ->setFinder($finder) 27 | ->setCacheFile(__DIR__ . '/var/.php-cs-fixer.cache') 28 | ->setRiskyAllowed(true) 29 | ->setRules([ 30 | '@PSR12' => true, 31 | '@PHP81Migration' => true, 32 | 'strict_param' => true, 33 | 'array_syntax' => ['syntax' => 'short'], 34 | 'octal_notation' => false, 35 | 'trim_array_spaces' => true, 36 | 'phpdoc_order' => true, 37 | 'ordered_imports' => true, 38 | 'new_with_parentheses' => true, 39 | 'method_chaining_indentation' => true, 40 | 'no_unused_imports' => true, 41 | 'align_multiline_comment' => true, 42 | 'array_indentation' => true, 43 | 'blank_line_after_opening_tag' => false, 44 | 'header_comment' => [ 45 | 'header' => $header, 46 | 'comment_type' => 'PHPDoc', 47 | 'location' => 'after_open', 48 | 'separate' => 'bottom', 49 | ], 50 | ]); 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | ## How to contribute step-by-step 4 | 5 | 1. Fork the `dbout/wp-orm` repository. 6 | 2. Create a new branch from `main` in your fork. This makes it easier for you to keep track of your changes. 7 | 3. Make the desired changes to the code. 8 | * If you are adding new functionality or fixing a bug, we recommend you add unit tests that cover it. 9 | 4. Push the changes to your fork. 10 | 5. Create a pull request to the `dbout/wp-orm` repository. 11 | 6. In your pull request, please describe in detail: 12 | * What problem you’re solving 13 | * Your approach to fixing the problem 14 | * Any tests you wrote 15 | 7. Check Allow edits from maintainers. 16 | 8. Create the pull request. 17 | 9. Ensure that all checks have passed. 18 | 19 | After you create your pull request, one of the code owners will review your code. 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dimitri BOUTEILLE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress ORM with Eloquent 2 | 3 | [![GitHub Release](https://img.shields.io/github/v/release/dimitriBouteille/wp-orm)](https://github.com/dimitriBouteille/wp-orm/releases) 4 | [![Tests](https://img.shields.io/github/actions/workflow/status/dimitriBouteille/wp-orm/tests.yml?label=tests)](https://github.com/dimitriBouteille/wp-orm/actions/workflows/tests.yml) 5 | [![Packagist Downloads](https://img.shields.io/packagist/dt/dbout/wp-orm?color=yellow)](https://packagist.org/packages/dbout/wp-orm) 6 | [![Eloquent version](https://img.shields.io/packagist/dependency-v/dbout/wp-orm/illuminate%2Fdatabase?color=orange)](https://github.com/dimitriBouteille/wp-orm/blob/main/composer.json) 7 | [![Coverage Status](https://coveralls.io/repos/github/dimitriBouteille/wp-orm/badge.svg?branch=main)](https://coveralls.io/github/dimitriBouteille/wp-orm) 8 | 9 | WordPress ORM with Eloquent is a small library that adds a basic ORM into WordPress, which is easily extendable and includes models for core WordPress models such as posts, post metas, users, comments and more. 10 | The ORM is based on [Eloquent ORM](https://laravel.com/docs/eloquent) and uses the WordPress connection (`wpdb` class). 11 | 12 | > [!TIP] 13 | > To simplify the integration of this library, we recommend using WordPress with one of the following tools: [Bedrock](https://roots.io/bedrock/), [Themosis](https://framework.themosis.com/) or [Wordplate](https://github.com/wordplate/wordplate#readme). 14 | 15 | ## Features 16 | 17 | - ✅ Support core WordPress models: `Comment`, `Option`, `Post`, `TermTaxonomy`, `Term`, `User`, `PostMeta` and `UserMeta` 18 | - ✅ Support core WordPress post type: `Article`, `Attachment` and `Page` 19 | - ✅ Based on core WordPress database connection (`wpdb` class), no configuration required ! 20 | - ✅ Custom functions to filter models with meta 21 | - ✅ Meta casting (e.g. [Attribute Casting](https://laravel.com/docs/eloquent-mutators#attribute-casting)) 22 | - ✅ Multisite support 23 | - ❤️ Easy integration of a custom post and comment type 24 | - ❤️ Easy model creation for projects with custom tables 25 | - ❤️ All the features available in Eloquent, are usable with this library ! 26 | 27 | **Not yet developed but planned in a future version:** 28 | 29 | - 🗓️ [Create migration tool with Eloquent](https://github.com/dimitriBouteille/wp-orm/issues/28) 30 | 31 | ## Documentation 32 | 33 | This documentation only covers the specific points of this library, if you want to know more about Eloquent, the easiest is to look at [the documentation of Eloquent](https://laravel.com/doc/eloquent). 34 | 35 | You can find all the documentation in [the wiki](https://github.com/dimitriBouteille/wp-orm/wiki). 36 | 37 | ## Installation 38 | 39 | **Requirements** 40 | 41 | The server requirements are basically the same as for [WordPress](https://wordpress.org/about/requirements/) with the addition of a few ones : 42 | 43 | - PHP >= 8.2 44 | - [Composer](https://getcomposer.org/) 45 | 46 | **Installation** 47 | 48 | You can use [Composer](https://getcomposer.org/). Follow the [installation instructions](https://getcomposer.org/doc/00-intro.md) if you do not already have composer installed. 49 | 50 | ~~~bash 51 | composer require dbout/wp-orm 52 | ~~~ 53 | 54 | In your `wp-config.php` make sure you include the autoloader: 55 | 56 | ~~~php 57 | require __DIR__ . '/vendor/autoload.php'; 58 | ~~~ 59 | 60 | 🎉 You have nothing more to do, you can use the library now! Not even need to configure database accesses because it's the `wpdb` connection that is used. 61 | 62 | ## Contributing 63 | 64 | We encourage you to contribute to this repository, so everyone can benefit from new features, bug fixes, and any other improvements. Have a look at our [contributing guidelines](CONTRIBUTING.md) to find out how to raise a pull request. 65 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbout/wp-orm", 3 | "description": "WordPress ORM with Eloquent.", 4 | "type": "package", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Dimitri BOUTEILLE", 9 | "email": "bonjour@dimitri-bouteille.fr", 10 | "homepage": "https://github.com/dimitriBouteille", 11 | "role": "Developer" 12 | } 13 | ], 14 | "keywords": ["wordpress", "wp", "orm", "database", "eloquent", "db", "sql", "migration"], 15 | "homepage": "https://github.com/dimitriBouteille/wp-orm", 16 | "support": { 17 | "issues": "https://github.com/dimitriBouteille/wp-orm/issues", 18 | "source": "https://github.com/dimitriBouteille/wp-orm/" 19 | }, 20 | "minimum-stability": "dev", 21 | "prefer-stable": true, 22 | "require": { 23 | "php": ">=8.2", 24 | "laravel/serializable-closure": ">=1.3", 25 | "illuminate/database": "^11.0", 26 | "illuminate/events": "^11.0" 27 | }, 28 | "autoload": { 29 | "files": [ 30 | "src/helpers.php" 31 | ], 32 | "psr-4": { 33 | "Dbout\\WpOrm\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Dbout\\WpOrm\\Tests\\": "tests/" 39 | } 40 | }, 41 | "require-dev": { 42 | "phpunit/phpunit": "^11.0", 43 | "yoast/phpunit-polyfills": "^3.0", 44 | "rector/rector": "^2.0", 45 | "phpstan/extension-installer": "^1.4", 46 | "szepeviktor/phpstan-wordpress": "^2.0", 47 | "friendsofphp/php-cs-fixer": "^3.68", 48 | "phpstan/phpstan": "^2.0", 49 | "roots/wordpress": "^6.8" 50 | }, 51 | "config": { 52 | "allow-plugins": { 53 | "roots/wordpress-core-installer": true, 54 | "phpstan/extension-installer": true 55 | } 56 | }, 57 | "extra": { 58 | "phpstan": { 59 | "includes": [ 60 | "extension.neon" 61 | ] 62 | }, 63 | "wordpress-install-dir": "web/wordpress" 64 | }, 65 | "scripts": { 66 | "rector": "vendor/bin/rector process src --dry-run", 67 | "phpstan": "vendor/bin/phpstan analyse -c phpstan.neon", 68 | "test:unit": "vendor/bin/phpunit --no-coverage", 69 | "test:unit:coverage": "vendor/bin/phpunit", 70 | "test:wordPress": "vendor/bin/phpunit -c phpunit-wp.xml --no-coverage", 71 | "test:wordPress:coverage": "vendor/bin/phpunit -c phpunit-wp.xml", 72 | "csFixer": "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", 73 | "fix:csFixer": "vendor/bin/php-cs-fixer fix" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/scripts/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ######################################################################## 4 | # Script to download and install WordPress for use in automated testing. 5 | # 6 | # Source: https://github.com/wp-cli/scaffold-command/blob/main/templates/install-wp-tests.sh 7 | # Last updated based on commit https://github.com/wp-cli/scaffold-command/commit/efdc0aebe792eaa7ddf6725eae45d70fe6c6ce2a 8 | # dated September 15 2024. 9 | ######################################################################## 10 | 11 | if [ $# -lt 3 ]; then 12 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 13 | exit 1 14 | fi 15 | 16 | DB_NAME=$1 17 | DB_USER=$2 18 | DB_PASS=$3 19 | DB_HOST=${4-localhost} 20 | WP_VERSION=${5-latest} 21 | SKIP_DB_CREATE=${6-false} 22 | 23 | TMPDIR=${TMPDIR-/tmp} 24 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 25 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 26 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 27 | 28 | download() { 29 | if [ `which curl` ]; then 30 | curl -s "$1" > "$2"; 31 | elif [ `which wget` ]; then 32 | wget -nv -O "$2" "$1" 33 | else 34 | echo "Error: Neither curl nor wget is installed." 35 | exit 1 36 | fi 37 | } 38 | 39 | # Check if svn is installed 40 | check_svn_installed() { 41 | if ! command -v svn > /dev/null; then 42 | echo "Error: svn is not installed. Please install svn and try again." 43 | exit 1 44 | fi 45 | } 46 | 47 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 48 | WP_BRANCH=${WP_VERSION%\-*} 49 | WP_TESTS_TAG="branches/$WP_BRANCH" 50 | 51 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 52 | WP_TESTS_TAG="branches/$WP_VERSION" 53 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 54 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 55 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 56 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 57 | else 58 | WP_TESTS_TAG="tags/$WP_VERSION" 59 | fi 60 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 61 | WP_TESTS_TAG="trunk" 62 | else 63 | # http serves a single offer, whereas https serves multiple. we only want one 64 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 65 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 66 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 67 | if [[ -z "$LATEST_VERSION" ]]; then 68 | echo "Latest WordPress version could not be found" 69 | exit 1 70 | fi 71 | WP_TESTS_TAG="tags/$LATEST_VERSION" 72 | fi 73 | set -ex 74 | 75 | install_wp() { 76 | 77 | if [ -d $WP_CORE_DIR ]; then 78 | return; 79 | fi 80 | 81 | mkdir -p $WP_CORE_DIR 82 | 83 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 84 | mkdir -p $TMPDIR/wordpress-trunk 85 | rm -rf $TMPDIR/wordpress-trunk/* 86 | check_svn_installed 87 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 88 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 89 | else 90 | if [ $WP_VERSION == 'latest' ]; then 91 | local ARCHIVE_NAME='latest' 92 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 93 | # https serves multiple offers, whereas http serves single. 94 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 95 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 96 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 97 | LATEST_VERSION=${WP_VERSION%??} 98 | else 99 | # otherwise, scan the releases and get the most up to date minor version of the major release 100 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 101 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 102 | fi 103 | if [[ -z "$LATEST_VERSION" ]]; then 104 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 105 | else 106 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 107 | fi 108 | else 109 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 110 | fi 111 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 112 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 113 | fi 114 | 115 | download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 116 | } 117 | 118 | install_test_suite() { 119 | # portable in-place argument for both GNU sed and Mac OSX sed 120 | if [[ $(uname -s) == 'Darwin' ]]; then 121 | local ioption='-i.bak' 122 | else 123 | local ioption='-i' 124 | fi 125 | 126 | # set up testing suite if it doesn't yet exist 127 | if [ ! -d $WP_TESTS_DIR ]; then 128 | # set up testing suite 129 | mkdir -p $WP_TESTS_DIR 130 | rm -rf $WP_TESTS_DIR/{includes,data} 131 | check_svn_installed 132 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 133 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 134 | fi 135 | 136 | if [ ! -f wp-tests-config.php ]; then 137 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 138 | # remove all forward slashes in the end 139 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 140 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 141 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 142 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 143 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 144 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 145 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 146 | fi 147 | 148 | } 149 | 150 | recreate_db() { 151 | shopt -s nocasematch 152 | if [[ $1 =~ ^(y|yes)$ ]] 153 | then 154 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 155 | create_db 156 | echo "Recreated the database ($DB_NAME)." 157 | else 158 | echo "Leaving the existing database ($DB_NAME) in place." 159 | fi 160 | shopt -u nocasematch 161 | } 162 | 163 | create_db() { 164 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 165 | } 166 | 167 | install_db() { 168 | 169 | if [ ${SKIP_DB_CREATE} = "true" ]; then 170 | return 0 171 | fi 172 | 173 | # parse DB_HOST for port or socket references 174 | local PARTS=(${DB_HOST//\:/ }) 175 | local DB_HOSTNAME=${PARTS[0]}; 176 | local DB_SOCK_OR_PORT=${PARTS[1]}; 177 | local EXTRA="" 178 | 179 | if ! [ -z $DB_HOSTNAME ] ; then 180 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 181 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 182 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 183 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 184 | elif ! [ -z $DB_HOSTNAME ] ; then 185 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 186 | fi 187 | fi 188 | 189 | # create database 190 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 191 | then 192 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 193 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB 194 | recreate_db $DELETE_EXISTING_DB 195 | else 196 | create_db 197 | fi 198 | } 199 | 200 | install_wp 201 | install_test_suite 202 | install_db -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | - tests 6 | excludePaths: 7 | - tests/WordPress/TestCase.php 8 | ignoreErrors: 9 | - 10 | identifier: requireOnce.fileNotFound 11 | -------------------------------------------------------------------------------- /phpunit-wp.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | tests/WordPress 23 | 24 | 25 | 26 | 27 | src 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | tests/Unit/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ./src 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | use Rector\Config\RectorConfig; 10 | use Rector\Set\ValueObject\SetList; 11 | use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromStrictConstructorRector; 12 | 13 | return RectorConfig::configure() 14 | ->withPaths([ 15 | __DIR__ . '/src', 16 | __DIR__ . '/tests', 17 | ]) 18 | ->withRules([ 19 | TypedPropertyFromStrictConstructorRector::class, 20 | ]) 21 | ->withPreparedSets( 22 | codeQuality: true, 23 | ) 24 | ->withSets([ 25 | SetList::PHP_82, 26 | ]); 27 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":disableDependencyDashboard" 6 | ], 7 | "minimumReleaseAge": "30 days", 8 | "labels": ["Dependency"], 9 | "prConcurrentLimit": 5, 10 | "baseBranches": ["main"], 11 | "assignees": ["dimitriBouteille"] 12 | } 13 | -------------------------------------------------------------------------------- /src/Api/CustomModelTypeInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Api; 10 | 11 | interface CustomModelTypeInterface 12 | { 13 | /** 14 | * @return string 15 | */ 16 | public function getCustomTypeCode(): string; 17 | 18 | /** 19 | * @return string 20 | */ 21 | public function getCustomTypeColumn(): string; 22 | } 23 | -------------------------------------------------------------------------------- /src/Api/WithMetaModelInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Api; 10 | 11 | use Dbout\WpOrm\MetaMappingConfig; 12 | 13 | interface WithMetaModelInterface 14 | { 15 | /** 16 | * @return MetaMappingConfig 17 | */ 18 | public function getMetaConfigMapping(): MetaMappingConfig; 19 | } 20 | -------------------------------------------------------------------------------- /src/Builders/AbstractBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Illuminate\Database\Eloquent\Builder; 12 | use Illuminate\Database\Eloquent\Model; 13 | 14 | abstract class AbstractBuilder extends Builder 15 | { 16 | /** 17 | * @inheritDoc 18 | */ 19 | public function setModel(Model $model) 20 | { 21 | parent::setModel($model); 22 | $this->select(sprintf('%s.*', $this->model->getTable())); 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * @param string $columns 29 | * @param array $value 30 | * @return $this 31 | */ 32 | protected function _whereOrIn(string $columns, array $value): self 33 | { 34 | $first = reset($value); 35 | if (is_array($first)) { 36 | $this->whereIn($columns, $first); 37 | } elseif (count($value) == 1) { 38 | $this->where($columns, reset($value)); 39 | } else { 40 | $this->whereIn($columns, $value); 41 | } 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param $query 48 | * @param $table 49 | * @return bool 50 | */ 51 | protected function joined($query, $table): bool 52 | { 53 | $joins = $query->getQuery()->joins; 54 | if ($joins == null) { 55 | return false; 56 | } 57 | 58 | foreach ($joins as $join) { 59 | if ($join->table == $table) { 60 | return true; 61 | } 62 | } 63 | 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Builders/AbstractWithMetaBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Dbout\WpOrm\Api\WithMetaModelInterface; 12 | use Dbout\WpOrm\Exceptions\MetaNotSupportedException; 13 | use Dbout\WpOrm\Exceptions\WpOrmException; 14 | use Dbout\WpOrm\MetaMappingConfig; 15 | use Dbout\WpOrm\Orm\AbstractModel; 16 | use Dbout\WpOrm\Orm\Database; 17 | use Illuminate\Database\Eloquent\Model; 18 | 19 | abstract class AbstractWithMetaBuilder extends AbstractBuilder 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected array $joinCallback = [ 25 | 'inner' => 'join', 26 | 'left' => 'leftJoin', 27 | 'right' => 'rightJoin', 28 | ]; 29 | 30 | /** 31 | * @var MetaMappingConfig|null 32 | */ 33 | protected ?MetaMappingConfig $metaConfig = null; 34 | 35 | /** 36 | * @var string|null 37 | */ 38 | protected ?string $metaTable = null; 39 | 40 | /** 41 | * @inheritDoc 42 | * @throws \Exception 43 | */ 44 | public function setModel(Model $model): self 45 | { 46 | parent::setModel($model); 47 | $this->initMeta(); 48 | return $this; 49 | } 50 | 51 | /** 52 | * @param string $metaKey 53 | * @param string|null $alias 54 | * @throws WpOrmException 55 | * @return $this 56 | */ 57 | public function addMetaToSelect(string $metaKey, ?string $alias = null): self 58 | { 59 | $this->joinToMeta($metaKey); 60 | if ($alias === null || $alias === '') { 61 | $alias = sprintf('%s_value', $metaKey); 62 | } 63 | 64 | $column = sprintf('%s.%s AS %s', $metaKey, $this->metaConfig?->columnValue, $alias); 65 | $this->addSelect($column); 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param array|array $metas 71 | * @throws WpOrmException 72 | * @return $this 73 | */ 74 | public function addMetasToSelect(array $metas): self 75 | { 76 | foreach ($metas as $key => $metaName) { 77 | $alias = null; 78 | if (is_string($key)) { 79 | $alias = $key; 80 | } 81 | 82 | $this->addMetaToSelect($metaName, $alias); 83 | } 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * @param string $metaKey 90 | * @param mixed $value 91 | * @param string $operator 92 | * @throws WpOrmException 93 | * @return $this 94 | */ 95 | public function addMetaToFilter(string $metaKey, mixed $value, string $operator = '='): self 96 | { 97 | $this 98 | ->joinToMeta($metaKey) 99 | ->where(sprintf('%s.%s', $metaKey, $this->metaConfig->columnValue), $operator, $value); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * @param string $metaKey 106 | * @param string $joinType 107 | * @throws WpOrmException 108 | * @return $this 109 | */ 110 | public function joinToMeta(string $metaKey, string $joinType = 'inner'): self 111 | { 112 | $model = $this->model; 113 | $joinTable = sprintf('%s AS %s', $this->metaTable, $metaKey); 114 | 115 | if ($this->joined($this, $joinTable)) { 116 | return $this; 117 | } 118 | 119 | $join = $this->joinCallback[$joinType] ?? null; 120 | if ($join === null) { 121 | throw new WpOrmException('Invalid join type.'); 122 | } 123 | 124 | $this->$join($joinTable, function ($join) use ($metaKey, $model) { 125 | /** @var \Illuminate\Database\Query\JoinClause $join */ 126 | $join->on( 127 | sprintf('%s.%s', $metaKey, $this->metaConfig?->columnKey), 128 | '=', 129 | Database::getInstance()->raw(sprintf("'%s'", $metaKey)) 130 | )->on( 131 | sprintf('%s.%s', $metaKey, $this->metaConfig?->foreignKey), 132 | '=', 133 | sprintf('%s.%s', $model->getTable(), $model->getKeyName()), 134 | ); 135 | }); 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * @throws \ReflectionException 142 | * @throws MetaNotSupportedException 143 | * @throws WpOrmException 144 | */ 145 | protected function initMeta(): void 146 | { 147 | if (!$this->model instanceof WithMetaModelInterface) { 148 | throw new MetaNotSupportedException(sprintf( 149 | 'Model %s must be implement %s', 150 | get_class($this->model), 151 | WithMetaModelInterface::class 152 | )); 153 | } 154 | 155 | $config = $this->model->getMetaConfigMapping(); 156 | $reflection = new \ReflectionClass($config->metaClass); 157 | if (!$reflection->isSubclassOf(AbstractModel::class)) { 158 | throw new WpOrmException(sprintf( 159 | 'Class %s must extend from %s.', 160 | $config->metaClass, 161 | AbstractModel::class 162 | )); 163 | } 164 | 165 | /** @var AbstractModel $metaModel */ 166 | $metaModel = $reflection->newInstanceWithoutConstructor(); 167 | 168 | $this->metaTable = $metaModel->getTable(); 169 | $this->metaConfig = $config; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Builders/CommentBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Dbout\WpOrm\Models\Comment; 12 | use Illuminate\Database\Eloquent\Collection; 13 | 14 | class CommentBuilder extends AbstractBuilder 15 | { 16 | /** 17 | * @param string $type 18 | * @return Collection 19 | */ 20 | public function findAllByType(string $type): Collection 21 | { 22 | return $this 23 | ->whereTypes([$type]) 24 | ->get(); 25 | } 26 | 27 | /** 28 | * @param mixed ...$types 29 | * @return $this 30 | */ 31 | public function whereTypes(...$types): self 32 | { 33 | return $this->_whereOrIn(Comment::TYPE, $types); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Builders/OptionBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Dbout\WpOrm\Models\Option; 12 | 13 | class OptionBuilder extends AbstractBuilder 14 | { 15 | /** 16 | * @param string $optionName 17 | * @return $this 18 | */ 19 | public function whereName(string $optionName): self 20 | { 21 | return $this->where(Option::NAME, $optionName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Builders/PostBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Dbout\WpOrm\Models\Post; 12 | use Illuminate\Database\Eloquent\Collection; 13 | 14 | class PostBuilder extends AbstractWithMetaBuilder 15 | { 16 | /** 17 | * @param string $type 18 | * @return Collection 19 | */ 20 | public function findAllByType(string $type): Collection 21 | { 22 | return $this 23 | ->whereTypes([$type]) 24 | ->get(); 25 | } 26 | 27 | /** 28 | * @param mixed ...$types 29 | * @return $this 30 | */ 31 | public function whereTypes(...$types): self 32 | { 33 | return $this->_whereOrIn(Post::TYPE, $types); 34 | } 35 | 36 | /** 37 | * @param $author 38 | * @return $this 39 | */ 40 | public function whereAuthor($author): self 41 | { 42 | $this->where(Post::AUTHOR, $author); 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param mixed ...$status 48 | * @return $this 49 | */ 50 | public function whereStatus(...$status): self 51 | { 52 | return $this->_whereOrIn(Post::STATUS, $status); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Builders/TermBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Dbout\WpOrm\Models\TermTaxonomy; 12 | use Illuminate\Database\Eloquent\Collection; 13 | 14 | class TermBuilder extends AbstractBuilder 15 | { 16 | /** 17 | * @param string $taxonomy 18 | * @return Collection 19 | */ 20 | public function findAllByTaxonomy(string $taxonomy): Collection 21 | { 22 | return $this->whereHas('termTaxonomy', function ($query) use ($taxonomy) { 23 | return $query->where(TermTaxonomy::TAXONOMY, $taxonomy); 24 | })->get(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Builders/UserBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Builders; 10 | 11 | use Dbout\WpOrm\Models\User; 12 | 13 | class UserBuilder extends AbstractWithMetaBuilder 14 | { 15 | /** 16 | * @param string $email 17 | * @return $this 18 | */ 19 | public function whereEmail(string $email): self 20 | { 21 | return $this->where(User::EMAIL, $email); 22 | } 23 | 24 | /** 25 | * @param string $login 26 | * @return $this 27 | */ 28 | public function whereLogin(string $login): self 29 | { 30 | return $this->where(User::LOGIN, $login); 31 | } 32 | 33 | /** 34 | * @param mixed ...$emails 35 | * @return $this 36 | */ 37 | public function whereEmails(... $emails): self 38 | { 39 | return $this->_whereOrIn(User::EMAIL, $emails); 40 | } 41 | 42 | /** 43 | * @param mixed ...$logins 44 | * @return $this 45 | */ 46 | public function whereLogins(... $logins): self 47 | { 48 | return $this->_whereOrIn(User::LOGIN, $logins); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Concerns/HasMetas.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Concerns; 10 | 11 | use Dbout\WpOrm\MetaMappingConfig; 12 | use Dbout\WpOrm\Models\Meta\AbstractMeta; 13 | use Illuminate\Database\Eloquent\Relations\HasMany; 14 | use Illuminate\Support\Collection as BaseCollection; 15 | 16 | trait HasMetas 17 | { 18 | /** 19 | * @var array 20 | */ 21 | protected array $_tmpMetas = []; 22 | 23 | /** 24 | * The metas that should be cast. 25 | * 26 | * @var array 27 | */ 28 | protected array $metaCasts = []; 29 | 30 | /** 31 | * The built-in, primitive cast types supported by Eloquent. 32 | * 33 | * @var string[] 34 | */ 35 | protected static array $primitiveMetaCastTypes = [ 36 | 'array', 37 | 'bool', 38 | 'boolean', 39 | 'collection', 40 | 'date', 41 | 'datetime', 42 | 'double', 43 | 'float', 44 | 'immutable_date', 45 | 'int', 46 | 'integer', 47 | 'json', 48 | 'object', 49 | 'string', 50 | 'timestamp', 51 | ]; 52 | 53 | /** 54 | * The cache of the converted meta cast types. 55 | * 56 | * @var array 57 | */ 58 | protected static array $metaCastTypeCache = []; 59 | 60 | /** 61 | * Initialize the trait. 62 | * 63 | * @return void 64 | */ 65 | protected function initializeHasMetas(): void 66 | { 67 | $this->metaCasts = $this->ensureCastsAreStringValues( 68 | array_merge($this->metaCasts, $this->metaCasts()), 69 | ); 70 | } 71 | 72 | /** 73 | * @return void 74 | */ 75 | protected static function bootHasMetas(): void 76 | { 77 | static::saved(function ($model) { 78 | $model->saveTmpMetas(); 79 | }); 80 | } 81 | 82 | /** 83 | * @return HasMany 84 | */ 85 | public function metas(): HasMany 86 | { 87 | return $this->hasMany($this->getMetaConfigMapping()->metaClass, $this->getMetaConfigMapping()->foreignKey); 88 | } 89 | 90 | /** 91 | * @param string $metaKey 92 | * @return AbstractMeta|null 93 | */ 94 | public function getMeta(string $metaKey): ?AbstractMeta 95 | { 96 | /** @var AbstractMeta $value */ 97 | // @phpstan-ignore-next-line 98 | $value = $this->metas()->firstWhere($this->getMetaConfigMapping()->columnKey, $metaKey); 99 | return $value; 100 | } 101 | 102 | /** 103 | * @param string $metaKey 104 | * @return mixed|null 105 | */ 106 | public function getMetaValue(string $metaKey): mixed 107 | { 108 | if (!$this->exists) { 109 | return $this->_tmpMetas[$metaKey] ?? null; 110 | } 111 | 112 | $meta = $this->getMeta($metaKey); 113 | if (!$meta instanceof AbstractMeta) { 114 | return $meta; 115 | } 116 | 117 | $value = $meta->getValue(); 118 | if (!$this->metaHasCast($metaKey)) { 119 | return $value; 120 | } 121 | 122 | // If the meta exists within the cast array, we will convert it to 123 | // an appropriate native PHP type dependent upon the associated value 124 | // given with the key in the pair. Dayle made this comment line up. 125 | return $this->castMeta($metaKey, $value); 126 | } 127 | 128 | /** 129 | * @param string $metaKey 130 | * @return bool 131 | */ 132 | public function hasMeta(string $metaKey): bool 133 | { 134 | // @phpstan-ignore-next-line 135 | return $this->metas() 136 | ->where($this->getMetaConfigMapping()->columnKey, $metaKey) 137 | ->exists(); 138 | } 139 | 140 | /** 141 | * @param string $metaKey 142 | * @param mixed $value 143 | * @return AbstractMeta|null 144 | */ 145 | public function setMeta(string $metaKey, mixed $value): ?AbstractMeta 146 | { 147 | if (!$this->exists) { 148 | $this->_tmpMetas[$metaKey] = $value; 149 | return null; 150 | } 151 | 152 | /** @var AbstractMeta $instance */ 153 | $instance = $this->metas() 154 | ->firstOrNew([ 155 | $this->getMetaConfigMapping()->columnKey => $metaKey, 156 | ]); 157 | 158 | $instance->fill([ 159 | $this->getMetaConfigMapping()->columnValue => $value, 160 | ])->save(); 161 | 162 | return $instance; 163 | } 164 | 165 | /** 166 | * @param string $metaKey 167 | * @return bool 168 | */ 169 | public function deleteMeta(string $metaKey): bool 170 | { 171 | if (!$this->exists) { 172 | unset($this->_tmpMetas[$metaKey]); 173 | return true; 174 | } 175 | 176 | // @phpstan-ignore-next-line 177 | return $this->metas() 178 | ->where($this->getMetaConfigMapping()->columnKey, $metaKey) 179 | ->forceDelete(); 180 | } 181 | 182 | /** 183 | * @return void 184 | */ 185 | protected function saveTmpMetas(): void 186 | { 187 | foreach ($this->_tmpMetas as $metaKey => $value) { 188 | $this->setMeta($metaKey, $value); 189 | } 190 | 191 | $this->_tmpMetas = []; 192 | } 193 | 194 | /** 195 | * @return MetaMappingConfig 196 | */ 197 | abstract public function getMetaConfigMapping(): MetaMappingConfig; 198 | 199 | /** 200 | * Get the metas that should be cast. 201 | * 202 | * @return array 203 | */ 204 | public function getMetaCasts(): array 205 | { 206 | return $this->metaCasts; 207 | } 208 | 209 | /** 210 | * Get the metas that should be cast. 211 | * 212 | * @return array 213 | */ 214 | protected function metaCasts(): array 215 | { 216 | return []; 217 | } 218 | 219 | /** 220 | * Cast a meta to a native PHP type. 221 | * 222 | * @param string $key 223 | * @param mixed $value 224 | * @return mixed 225 | */ 226 | protected function castMeta(string $key, mixed $value): mixed 227 | { 228 | $castType = $this->getMetaCastType($key); 229 | if (is_null($value) && in_array($castType, static::$primitiveMetaCastTypes, true)) { 230 | return null; 231 | } 232 | 233 | switch ($castType) { 234 | case 'int': 235 | case 'integer': 236 | return (int)$value; 237 | case 'real': 238 | case 'float': 239 | case 'double': 240 | return (float)$value; 241 | case 'string': 242 | return (string)$value; 243 | case 'bool': 244 | case 'boolean': 245 | return (bool) $value; 246 | case 'array': 247 | case 'json': 248 | return $this->fromJson($value); 249 | case 'object': 250 | return $this->fromJson($value, true); 251 | case 'collection': 252 | return new BaseCollection($this->fromJson($value)); 253 | case 'date': 254 | return $this->asDate($value); 255 | case 'datetime': 256 | return $this->asDateTime($value); 257 | case 'immutable_date': 258 | return $this->asDate($value)->toImmutable(); 259 | case 'timestamp': 260 | return $this->asTimestamp($value); 261 | } 262 | 263 | if ($this->isEnumMetaCastable($key)) { 264 | return $this->getEnumCastableMetaValue($key, $value); 265 | } 266 | 267 | /** 268 | * @todo Support custom class cast 269 | */ 270 | 271 | return $value; 272 | } 273 | 274 | /** 275 | * Determine if the given key is cast using an enum. 276 | * 277 | * @param string $key 278 | * @return bool 279 | */ 280 | protected function isEnumMetaCastable(string $key): bool 281 | { 282 | $casts = $this->getMetaCasts(); 283 | if (!array_key_exists($key, $casts)) { 284 | return false; 285 | } 286 | 287 | $castType = $casts[$key]; 288 | if (in_array($castType, static::$primitiveMetaCastTypes, true)) { 289 | return false; 290 | } 291 | 292 | return enum_exists($castType); 293 | } 294 | 295 | /** 296 | * Cast the given meta to an enum. 297 | * 298 | * @param string $key 299 | * @param mixed $value 300 | * @return \UnitEnum|\BackedEnum|null 301 | */ 302 | protected function getEnumCastableMetaValue(string $key, mixed $value): null|\UnitEnum|\BackedEnum 303 | { 304 | if (is_null($value)) { 305 | return null; 306 | } 307 | 308 | $castType = $this->getMetaCasts()[$key]; 309 | if ($value instanceof $castType) { 310 | return $value; 311 | } 312 | 313 | return $this->getEnumCaseFromValue($castType, $value); 314 | } 315 | 316 | /** 317 | * Determine whether a meta should be cast to a native type. 318 | * 319 | * @param string $key 320 | * @param string|null $types 321 | * @return bool 322 | */ 323 | public function metaHasCast(string $key, string $types = null): bool 324 | { 325 | if (array_key_exists($key, $this->getMetaCasts())) { 326 | return !$types || in_array($this->getMetaCastType($key), (array)$types, true); 327 | } 328 | 329 | return false; 330 | } 331 | 332 | /** 333 | * Get the type of cast for a meta. 334 | * 335 | * @param string $key 336 | * @return string 337 | */ 338 | protected function getMetaCastType(string $key): string 339 | { 340 | $castType = $this->getMetaCasts()[$key] ?? null; 341 | if (isset(static::$metaCastTypeCache[$castType])) { 342 | return static::$metaCastTypeCache[$castType]; 343 | } 344 | 345 | if ($this->isCustomDateTimeCast($castType)) { 346 | $convertedCastType = 'custom_datetime'; 347 | } elseif ($this->isImmutableCustomDateTimeCast($castType)) { 348 | $convertedCastType = 'immutable_custom_datetime'; 349 | } elseif ($this->isDecimalCast($castType)) { 350 | $convertedCastType = 'decimal'; 351 | } elseif (class_exists($castType)) { 352 | $convertedCastType = $castType; 353 | } else { 354 | $convertedCastType = trim(strtolower((string)$castType)); 355 | } 356 | 357 | return static::$metaCastTypeCache[$castType] = $convertedCastType; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/Enums/PingStatus.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Enums; 10 | 11 | enum PingStatus: string 12 | { 13 | case Closed = 'closed'; 14 | case Open = 'open'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Enums/PostStatus.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Enums; 10 | 11 | /** 12 | * @see https://wordpress.org/documentation/article/post-status/ 13 | */ 14 | enum PostStatus: string 15 | { 16 | case Publish = 'publish'; 17 | case Future = 'future'; 18 | case Draft = 'draft'; 19 | case Pending = 'pending'; 20 | case Private = 'private'; 21 | case Trash = 'trash'; 22 | case AutoDraft = 'auto-draft'; 23 | case Inherit = 'inherit'; 24 | } 25 | -------------------------------------------------------------------------------- /src/Enums/YesNo.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Enums; 10 | 11 | enum YesNo: string 12 | { 13 | case Yes = 'yes'; 14 | case No = 'no'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/CannotOverrideCustomTypeException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Exceptions; 10 | 11 | class CannotOverrideCustomTypeException extends NotAllowedException 12 | { 13 | /** 14 | * @param string $type 15 | * @param int $code 16 | * @param \Throwable|null $previous 17 | */ 18 | public function __construct(string $type, int $code = 0, ?\Throwable $previous = null) 19 | { 20 | $message = sprintf( 21 | 'You cannot override type for this object. Current type [%s]', 22 | $type 23 | ); 24 | 25 | parent::__construct($message, $code, $previous); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exceptions/MetaNotSupportedException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Exceptions; 10 | 11 | class MetaNotSupportedException extends WpOrmException 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/NotAllowedException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Exceptions; 10 | 11 | class NotAllowedException extends WpOrmException 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/WpOrmException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Exceptions; 10 | 11 | class WpOrmException extends \Exception 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/MetaMappingConfig.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm; 10 | 11 | class MetaMappingConfig 12 | { 13 | /** 14 | * @param string $metaClass Meta className 15 | * @param string $foreignKey 16 | * @param string $columnKey Column contains the meta key 17 | * @param string $columnValue Column contains the meta value 18 | */ 19 | public function __construct( 20 | public readonly string $metaClass, 21 | public readonly string $foreignKey, 22 | public readonly string $columnKey = 'meta_key', 23 | public readonly string $columnValue = 'meta_value', 24 | ) { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/Article.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | class Article extends CustomPost 12 | { 13 | /** 14 | * @inheritDoc 15 | */ 16 | protected string $_type = 'post'; 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/Attachment.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | class Attachment extends CustomPost 12 | { 13 | /** 14 | * @inheritDoc 15 | */ 16 | protected string $_type = 'attachment'; 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/Comment.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Builders\CommentBuilder; 13 | use Dbout\WpOrm\Orm\AbstractModel; 14 | use Illuminate\Database\Eloquent\Relations\HasOne; 15 | 16 | /** 17 | * @method Comment setCommentAuthor(?string $author) 18 | * @method string|null getCommentAuthor() 19 | * @method Comment setCommentAuthorEmail(?string $email) 20 | * @method string|null getCommentAuthorEmail() 21 | * @method Comment setCommentAuthorUrl(?string $url) 22 | * @method string|null getCommentAuthorUrl() 23 | * @method Comment setCommentAuthorIP(?string $ip) 24 | * @method string|null getCommentAuthorIP() 25 | * @method Comment setCommentContent(?string $content) 26 | * @method string|null getCommentContent() 27 | * @method Comment setCommentKarma(?int $karma) 28 | * @method int|null getCommentKarma() 29 | * @method Comment setCommentApproved(string $approved) 30 | * @method string getCommentApproved() 31 | * @method Comment setCommentAgent(?string $agent) 32 | * @method string|null getCommentAgent() 33 | * @method Comment setCommentType(?string $type) 34 | * @method string|null getCommentType() 35 | * @method Comment setUserId(?int $userId) 36 | * @method int|null getUserId() 37 | * @method Comment setCommentDate(mixed $date) 38 | * @method Carbon|null getCommentDate() 39 | * @method Comment setCommentDateGmt(mixed $date) 40 | * @method Carbon|null getCommentDateGmt() 41 | * @method Comment setCommentPostID(int $postId) 42 | * @method int|null getCommentPostID() 43 | * @method Comment setCommentParent(?int $parentId) 44 | * @method int|null getCommentParent() 45 | * @method static CommentBuilder query() 46 | * 47 | * @property-read User|null $user 48 | * @property-read Post|null $post 49 | * @property-read Comment|null $parent 50 | */ 51 | class Comment extends AbstractModel 52 | { 53 | final public const CREATED_AT = self::DATE; 54 | final public const UPDATED_AT = null; 55 | final public const COMMENT_ID = 'comment_ID'; 56 | final public const POST_ID = 'comment_post_ID'; 57 | final public const AUTHOR = 'comment_author'; 58 | final public const AUTHOR_EMAIL = 'comment_author_email'; 59 | final public const AUTHOR_URL = 'comment_author_url'; 60 | final public const AUTHOR_IP = 'comment_author_IP'; 61 | final public const DATE = 'comment_date'; 62 | final public const DATE_GMT = 'comment_date_gmt'; 63 | final public const CONTENT = 'comment_content'; 64 | final public const KARMA = 'comment_karma'; 65 | final public const APPROVED = 'comment_approved'; 66 | final public const AGENT = 'comment_agent'; 67 | final public const TYPE = 'comment_type'; 68 | final public const PARENT = 'comment_parent'; 69 | final public const USER_ID = 'user_id'; 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | protected $primaryKey = self::COMMENT_ID; 75 | 76 | /** 77 | * @inheritDoc 78 | */ 79 | protected $table = 'comments'; 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | protected $casts = [ 85 | self::USER_ID => 'integer', 86 | self::POST_ID => 'integer', 87 | self::PARENT => 'integer', 88 | self::KARMA => 'integer', 89 | self::DATE_GMT => 'datetime', 90 | self::DATE => 'datetime', 91 | ]; 92 | 93 | /** 94 | * @return HasOne 95 | */ 96 | public function user(): HasOne 97 | { 98 | return $this->hasOne(User::class, User::USER_ID, self::USER_ID); 99 | } 100 | 101 | /** 102 | * @return HasOne 103 | */ 104 | public function post(): HasOne 105 | { 106 | return $this->hasOne(Post::class, Post::POST_ID, self::POST_ID); 107 | } 108 | 109 | /** 110 | * @return HasOne 111 | */ 112 | public function parent(): HasOne 113 | { 114 | return $this->hasOne(Comment::class, Comment::COMMENT_ID, self::PARENT); 115 | } 116 | 117 | /** 118 | * @inheritDoc 119 | */ 120 | public function newEloquentBuilder($query): CommentBuilder 121 | { 122 | return new CommentBuilder($query); 123 | } 124 | 125 | /** 126 | * @return int|null 127 | * @see getCommentPostID() 128 | */ 129 | public function getCommentPostIdAttribute(): ?int 130 | { 131 | return $this->getAttributes()[self::POST_ID] ?? null; 132 | } 133 | 134 | /** 135 | * @param int|null $postId 136 | * @return $this 137 | * @see setCommentPostID() 138 | */ 139 | public function setCommentPostIdAttribute(?int $postId): self 140 | { 141 | $this->attributes[self::POST_ID] = $postId; 142 | return $this; 143 | } 144 | 145 | /** 146 | * @return string|null 147 | * @see getCommentAuthorIp() 148 | */ 149 | public function getCommentAuthorIpAttribute(): ?string 150 | { 151 | return $this->getAttributes()[self::AUTHOR_IP] ?? null; 152 | } 153 | 154 | /** 155 | * @param mixed $ip 156 | * @return self 157 | * @see setCommentAuthorIp() 158 | */ 159 | public function setCommentAuthorIpAttribute(mixed $ip): self 160 | { 161 | $this->attributes[self::AUTHOR_IP] = $ip; 162 | return $this; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Models/CustomComment.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Dbout\WpOrm\Api\CustomModelTypeInterface; 12 | use Dbout\WpOrm\Exceptions\CannotOverrideCustomTypeException; 13 | use Dbout\WpOrm\Exceptions\NotAllowedException; 14 | use Dbout\WpOrm\Scopes\CustomModelTypeScope; 15 | 16 | abstract class CustomComment extends Comment implements CustomModelTypeInterface 17 | { 18 | /** 19 | * @var string 20 | */ 21 | protected string $_type; 22 | 23 | /** 24 | * @param array $attributes 25 | */ 26 | public function __construct(array $attributes = []) 27 | { 28 | parent::__construct(array_merge($attributes, [ 29 | self::TYPE => $this->_type, 30 | ])); 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function getCommentType(): ?string 37 | { 38 | return $this->_type; 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | public function getCustomTypeCode(): string 45 | { 46 | return $this->getCommentType(); 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function getCustomTypeColumn(): string 53 | { 54 | return self::TYPE; 55 | } 56 | 57 | /** 58 | * @param string $type 59 | * @throws NotAllowedException 60 | * @return never 61 | */ 62 | final public function setCommentType(string $type): never 63 | { 64 | throw new CannotOverrideCustomTypeException($this->_type); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | protected static function booted(): void 71 | { 72 | static::addGlobalScope(new CustomModelTypeScope()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Models/CustomPost.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Dbout\WpOrm\Api\CustomModelTypeInterface; 12 | use Dbout\WpOrm\Exceptions\CannotOverrideCustomTypeException; 13 | use Dbout\WpOrm\Exceptions\NotAllowedException; 14 | use Dbout\WpOrm\Scopes\CustomModelTypeScope; 15 | 16 | abstract class CustomPost extends Post implements CustomModelTypeInterface 17 | { 18 | /** 19 | * @var string 20 | */ 21 | protected string $_type; 22 | 23 | /** 24 | * @param array $attributes 25 | */ 26 | public function __construct(array $attributes = []) 27 | { 28 | parent::__construct(array_merge($attributes, [ 29 | self::TYPE => $this->_type, 30 | ])); 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function getPostType(): ?string 37 | { 38 | return $this->_type; 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | public function getCustomTypeCode(): string 45 | { 46 | return $this->getPostType(); 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function getCustomTypeColumn(): string 53 | { 54 | return self::TYPE; 55 | } 56 | 57 | /** 58 | * @param string $type 59 | * @throws NotAllowedException 60 | * @return never 61 | */ 62 | final public function setPostType(string $type): never 63 | { 64 | throw new CannotOverrideCustomTypeException($this->_type); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | protected static function booted(): void 71 | { 72 | static::addGlobalScope(new CustomModelTypeScope()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Models/Meta/AbstractMeta.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Meta; 10 | 11 | use Dbout\WpOrm\Orm\AbstractModel; 12 | 13 | abstract class AbstractMeta extends AbstractModel 14 | { 15 | final public const META_KEY = 'meta_key'; 16 | final public const META_VALUE = 'meta_value'; 17 | 18 | /** 19 | * Disable created_at and updated_at 20 | * @var bool 21 | */ 22 | public $timestamps = false; 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | protected $fillable = [ 28 | self::META_VALUE, 29 | self::META_KEY, 30 | ]; 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getKey(): string 36 | { 37 | return $this->getAttribute(self::META_KEY); 38 | } 39 | 40 | /** 41 | * @param string $key 42 | * @return $this 43 | */ 44 | public function setKey(string $key): self 45 | { 46 | $this->setAttribute(self::META_KEY, $key); 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return mixed 52 | */ 53 | public function getValue() 54 | { 55 | return $this->getAttribute(self::META_VALUE); 56 | } 57 | 58 | /** 59 | * @param string $value 60 | * @return $this 61 | */ 62 | public function setValue(string $value): self 63 | { 64 | $this->setAttribute(self::META_VALUE, $value); 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Models/Meta/MetaInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Meta; 10 | 11 | interface MetaInterface 12 | { 13 | /** 14 | * @return string 15 | */ 16 | public static function getMetaFkColumn(): string; 17 | 18 | /** 19 | * @return string 20 | */ 21 | public static function getMetaKeyColumn(): string; 22 | 23 | /** 24 | * @return string 25 | */ 26 | public static function getMetaValueColumn(): string; 27 | } 28 | -------------------------------------------------------------------------------- /src/Models/Meta/PostMeta.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Meta; 10 | 11 | use Dbout\WpOrm\Models\Post; 12 | use Illuminate\Database\Eloquent\Relations\HasOne; 13 | 14 | /** 15 | * @property-read Post|null $post 16 | */ 17 | class PostMeta extends AbstractMeta 18 | { 19 | final public const META_ID = 'meta_id'; 20 | final public const POST_ID = 'post_id'; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $table = 'postmeta'; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $primaryKey = self::META_ID; 31 | 32 | /** 33 | * @return HasOne 34 | */ 35 | public function post(): HasOne 36 | { 37 | return $this->hasOne(Post::class, Post::POST_ID, self::POST_ID); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Models/Meta/UserMeta.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Meta; 10 | 11 | use Dbout\WpOrm\Models\User; 12 | use Illuminate\Database\Eloquent\Relations\HasOne; 13 | 14 | /** 15 | * @property-read User|null $user 16 | */ 17 | class UserMeta extends AbstractMeta 18 | { 19 | final public const META_ID = 'umeta_id'; 20 | final public const USER_ID = 'user_id'; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $primaryKey = self::META_ID; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $table = 'usermeta'; 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | protected bool $useBasePrefix = true; 36 | 37 | /** 38 | * @return HasOne 39 | */ 40 | public function user(): HasOne 41 | { 42 | return $this->hasOne(User::class, User::USER_ID, self::USER_ID); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Models/Multisite/Blog.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Multisite; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Orm\AbstractModel; 13 | use Illuminate\Database\Eloquent\Relations\BelongsTo; 14 | use Illuminate\Database\Eloquent\Relations\HasOne; 15 | 16 | /** 17 | * @method int getSiteId() 18 | * @method Blog setSiteId(int $siteId) 19 | * @method string getDomain() 20 | * @method Blog setDomain(string $domain) 21 | * @method string getPath() 22 | * @method Blog setPath(string $path) 23 | * @method Carbon getRegistered() 24 | * @method Blog setRegistered($registered) 25 | * @method Carbon getLastUpdated() 26 | * @method Blog setLastUpdated($lastUpdated) 27 | * @method bool getPublic() 28 | * @method Blog setPublic(bool $public) 29 | * @method bool getArchived() 30 | * @method Blog setArchived(bool $archived) 31 | * @method bool getMature() 32 | * @method Blog setMature(bool $mature) 33 | * @method bool getSpam() 34 | * @method Blog setSpam(bool $spam) 35 | * @method bool getDeleted() 36 | * @method Blog setDeleted(bool $deleted) 37 | * @method int getLangId() 38 | * @method Blog setLangId(int $langId) 39 | * 40 | * @property-read Site $site 41 | * @property-read BlogVersion|null $version 42 | */ 43 | class Blog extends AbstractModel 44 | { 45 | public const CREATED_AT = self::REGISTERED; 46 | public const UPDATED_AT = self::LAST_UPDATED; 47 | 48 | final public const BLOG_ID = 'blog_id'; 49 | final public const SITE_ID = 'site_id'; 50 | final public const DOMAIN = 'domain'; 51 | final public const PATH = 'path'; 52 | final public const REGISTERED = 'registered'; 53 | final public const LAST_UPDATED = 'last_updated'; 54 | final public const PUBLIC = 'public'; 55 | final public const ARCHIVED = 'archived'; 56 | final public const MATURE = 'mature'; 57 | final public const SPAM = 'spam'; 58 | final public const DELETED = 'deleted'; 59 | final public const LANG_ID = 'lang_id'; 60 | 61 | protected $primaryKey = self::BLOG_ID; 62 | 63 | protected bool $useBasePrefix = true; 64 | 65 | protected $casts = [ 66 | self::BLOG_ID => 'int', 67 | self::SITE_ID => 'int', 68 | self::REGISTERED => 'datetime', 69 | self::LAST_UPDATED => 'datetime', 70 | self::PUBLIC => 'bool', 71 | self::ARCHIVED => 'bool', 72 | self::MATURE => 'bool', 73 | self::SPAM => 'bool', 74 | self::DELETED => 'bool', 75 | self::LANG_ID => 'int', 76 | ]; 77 | 78 | protected $table = 'blogs'; 79 | 80 | public function site(): BelongsTo 81 | { 82 | return $this->belongsTo(Site::class, self::SITE_ID); 83 | } 84 | 85 | public function version(): HasOne 86 | { 87 | return $this->hasOne(BlogVersion::class, BlogVersion::BLOG_ID); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Models/Multisite/BlogVersion.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Multisite; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Orm\AbstractModel; 13 | use Illuminate\Database\Eloquent\Relations\BelongsTo; 14 | 15 | /** 16 | * @method string getDbVersion() 17 | * @method BlogVersion setDbVersion(string $dbVersion) 18 | * @method Carbon getLastUpdated() 19 | * @method BlogVersion setLastUpdated($lastUpdated) 20 | * 21 | * @property-read Blog $blog 22 | */ 23 | class BlogVersion extends AbstractModel 24 | { 25 | public const CREATED_AT = null; 26 | public const UPDATED_AT = self::LAST_UPDATED; 27 | 28 | final public const BLOG_ID = 'blog_id'; 29 | final public const DB_VERSION = 'db_version'; 30 | final public const LAST_UPDATED = 'last_updated'; 31 | 32 | protected bool $useBasePrefix = true; 33 | 34 | protected $table = 'blog_versions'; 35 | 36 | protected $primaryKey = self::BLOG_ID; 37 | 38 | protected $casts = [ 39 | self::LAST_UPDATED => 'datetime', 40 | ]; 41 | 42 | public function blog(): BelongsTo 43 | { 44 | return $this->belongsTo(Blog::class, self::BLOG_ID); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/Multisite/RegistrationLog.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Multisite; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Orm\AbstractModel; 13 | use Illuminate\Database\Eloquent\Relations\BelongsTo; 14 | 15 | /** 16 | * @method string getEmail() 17 | * @method RegistrationLog setEmail(string $email) 18 | * @method string getIP() 19 | * @method RegistrationLog setIP(string $ip) 20 | * @method int getBlogId() 21 | * @method RegistrationLog setBlogId(int $blogId) 22 | * @method Carbon getDateRegistered() 23 | * @method RegistrationLog setDateRegistered($dateRegistered) 24 | * 25 | * @property-read Blog|null $blog 26 | */ 27 | class RegistrationLog extends AbstractModel 28 | { 29 | public const CREATED_AT = self::DATE_REGISTERED; 30 | public const UPDATED_AT = null; 31 | 32 | final public const ID = 'ID'; 33 | final public const EMAIL = 'email'; 34 | final public const IP = 'IP'; 35 | final public const BLOG_ID = 'blog_id'; 36 | final public const DATE_REGISTERED = 'date_registered'; 37 | 38 | protected bool $useBasePrefix = true; 39 | 40 | protected $table = 'registration_log'; 41 | 42 | protected $primaryKey = self::ID; 43 | 44 | protected $casts = [ 45 | self::BLOG_ID => 'int', 46 | self::DATE_REGISTERED => 'datetime', 47 | ]; 48 | 49 | public function blog(): BelongsTo 50 | { 51 | return $this->belongsTo(Blog::class, self::BLOG_ID); 52 | } 53 | 54 | /** 55 | * @see getIP() 56 | */ 57 | public function getIpAttribute(): ?string 58 | { 59 | return $this->getAttributes()[self::IP] ?? null; 60 | } 61 | 62 | /** 63 | * @see setIP() 64 | */ 65 | public function setIpAttribute(mixed $ip): self 66 | { 67 | $this->attributes[self::IP] = $ip; 68 | return $this; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Models/Multisite/Signup.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Multisite; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Models\User; 13 | use Dbout\WpOrm\Orm\AbstractModel; 14 | use Illuminate\Database\Eloquent\Relations\HasOne; 15 | 16 | /** 17 | * @method string getDomain() 18 | * @method Signup setDomain(string $domain) 19 | * @method string getPath() 20 | * @method Signup setPath(string $path) 21 | * @method string getTitle() 22 | * @method Signup setTitle(string $title) 23 | * @method string getUserLogin() 24 | * @method Signup setUserLogin(string $userLogin) 25 | * @method string getUserEmail() 26 | * @method Signup setUserEmail(string $userEmail) 27 | * @method Carbon getRegistered() 28 | * @method Signup setRegistered($registered) 29 | * @method Carbon getActivated() 30 | * @method Signup setActivated($activated) 31 | * @method bool getActive() 32 | * @method Signup setActive(bool $active) 33 | * @method string getActivationKey() 34 | * @method Signup setActivationKey(string $activationKey) 35 | * @method string|null getMeta() 36 | * @method Signup setMeta(?string $meta) 37 | * 38 | * @property-read User|null $user 39 | */ 40 | class Signup extends AbstractModel 41 | { 42 | public const CREATED_AT = self::REGISTERED; 43 | public const UPDATED_AT = null; 44 | 45 | final public const SIGNUP_ID = 'signup_id'; 46 | final public const DOMAIN = 'domain'; 47 | final public const PATH = 'path'; 48 | final public const TITLE = 'title'; 49 | final public const USER_LOGIN = 'user_login'; 50 | final public const USER_EMAIL = 'user_email'; 51 | final public const REGISTERED = 'registered'; 52 | final public const ACTIVATED = 'activated'; 53 | final public const ACTIVE = 'active'; 54 | final public const ACTIVATION_KEY = 'activation_key'; 55 | final public const META = 'meta'; 56 | 57 | protected bool $useBasePrefix = true; 58 | 59 | protected $table = 'signups'; 60 | 61 | protected $primaryKey = self::SIGNUP_ID; 62 | 63 | protected $casts = [ 64 | self::REGISTERED => 'datetime', 65 | self::ACTIVATED => 'datetime', 66 | self::ACTIVE => 'bool', 67 | ]; 68 | 69 | public function user(): HasOne 70 | { 71 | return $this->hasOne(User::class, User::EMAIL, self::USER_EMAIL); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Models/Multisite/Site.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Multisite; 10 | 11 | use Dbout\WpOrm\Concerns\HasMetas; 12 | use Dbout\WpOrm\MetaMappingConfig; 13 | use Dbout\WpOrm\Orm\AbstractModel; 14 | use Illuminate\Database\Eloquent\Collection; 15 | use Illuminate\Database\Eloquent\Relations\HasMany; 16 | 17 | /** 18 | * @method string getDomain() 19 | * @method Site setDomain(string $domain) 20 | * @method string getPath() 21 | * @method Site setPath(string $path) 22 | * 23 | * @property-read Collection $metas 24 | * @property-read Collection $blogs 25 | */ 26 | class Site extends AbstractModel 27 | { 28 | use HasMetas; 29 | 30 | public const CREATED_AT = null; 31 | public const UPDATED_AT = null; 32 | 33 | final public const ID = 'id'; 34 | final public const DOMAIN = 'domain'; 35 | final public const PATH = 'path'; 36 | 37 | protected bool $useBasePrefix = true; 38 | 39 | protected $table = 'site'; 40 | 41 | protected $primaryKey = self::ID; 42 | 43 | public function blogs(): HasMany 44 | { 45 | return $this->hasMany(Blog::class, Blog::SITE_ID); 46 | } 47 | 48 | public function getMetaConfigMapping(): MetaMappingConfig 49 | { 50 | return new MetaMappingConfig(SiteMeta::class, SiteMeta::SITE_ID); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Models/Multisite/SiteMeta.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models\Multisite; 10 | 11 | use Dbout\WpOrm\Models\Meta\AbstractMeta; 12 | use Illuminate\Database\Eloquent\Relations\HasOne; 13 | 14 | /** 15 | * @method int getSiteId() 16 | * @method SiteMeta setSiteId(int $siteId) 17 | * @method string|null getMetaKey() 18 | * @method SiteMeta setMetaKey(?string $metaKey) 19 | * @method mixed|null getMetaValue() 20 | * @method SiteMeta setMetaValue($metaValue) 21 | * 22 | * @property-read Site $site 23 | */ 24 | class SiteMeta extends AbstractMeta 25 | { 26 | final public const META_ID = 'meta_id'; 27 | final public const SITE_ID = 'site_id'; 28 | 29 | protected bool $useBasePrefix = true; 30 | 31 | protected $table = 'sitemeta'; 32 | 33 | protected $primaryKey = self::META_ID; 34 | 35 | public function site(): HasOne 36 | { 37 | return $this->hasOne(Site::class, Site::ID, self::SITE_ID); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Models/Option.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Dbout\WpOrm\Builders\OptionBuilder; 12 | use Dbout\WpOrm\Enums\YesNo; 13 | use Dbout\WpOrm\Orm\AbstractModel; 14 | 15 | /** 16 | * @method Option setOptionName(string $name) 17 | * @method string getOptionName() 18 | * @method Option setOptionValue($value) 19 | * @method mixed getOptionValue() 20 | * @method Option setAutoload(string|YesNo $autoload) 21 | * @method string getAutoload() 22 | * @method static OptionBuilder query() 23 | */ 24 | class Option extends AbstractModel 25 | { 26 | final public const OPTION_ID = 'option_id'; 27 | final public const NAME = 'option_name'; 28 | final public const VALUE = 'option_value'; 29 | final public const AUTOLOAD = 'autoload'; 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | protected $primaryKey = self::OPTION_ID; 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | protected $table = 'options'; 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | public $timestamps = false; 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function newEloquentBuilder($query): OptionBuilder 50 | { 51 | return new OptionBuilder($query); 52 | } 53 | 54 | /** 55 | * @param string $optionName 56 | * @return self|null 57 | */ 58 | public static function findOneByName(string $optionName): ?self 59 | { 60 | /** @var self|null $result */ 61 | $result = self::query()->firstWhere(self::NAME, $optionName); 62 | return $result; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Models/Page.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | class Page extends CustomPost 12 | { 13 | /** 14 | * @inheritDoc 15 | */ 16 | protected string $_type = 'page'; 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/Post.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Api\WithMetaModelInterface; 13 | use Dbout\WpOrm\Builders\PostBuilder; 14 | use Dbout\WpOrm\Concerns\HasMetas; 15 | use Dbout\WpOrm\MetaMappingConfig; 16 | use Dbout\WpOrm\Models\Meta\PostMeta; 17 | use Dbout\WpOrm\Orm\AbstractModel; 18 | use Illuminate\Database\Eloquent\Relations\HasMany; 19 | use Illuminate\Database\Eloquent\Relations\HasOne; 20 | use Illuminate\Support\Collection; 21 | 22 | /** 23 | * @method Post setPostDate($date) 24 | * @method Carbon|null getPostDate() 25 | * @method Post setPostDateGMT($date) 26 | * @method Carbon|null getPostDateGMT() 27 | * @method Post setPostContent(?string $content) 28 | * @method string|null getPostContent() 29 | * @method Post setPostType(string $type) 30 | * @method string|null getPostType() 31 | * @method Post setGuid(?string $guid) 32 | * @method string|null getGuid() 33 | * @method Post setPostTitle(?string $title) 34 | * @method string|null getPostTitle() 35 | * @method Post setPostExcerpt(?string $excerpt) 36 | * @method string|null getPostExcerpt() 37 | * @method Post setPostStatus(?string $status) 38 | * @method string|null getPostStatus() 39 | * @method Post setCommentStatus(string $status) 40 | * @method string|null getCommentStatus() 41 | * @method Post setPingStatus(string $status) 42 | * @method string|null getPingStatus() 43 | * @method Post setPostPassword(?string $password) 44 | * @method string|null getPostPassword() 45 | * @method Post setPostName(?string $name) 46 | * @method string|null getPostName() 47 | * @method Post setToPing(?string $toPing) 48 | * @method string|null getToPing() 49 | * @method Post setPinged(?string $pinged) 50 | * @method string|null getPinged() 51 | * @method Post setPostModified($modified) 52 | * @method Carbon|null getPostModified() 53 | * @method Post setPostModifiedGMT($modified) 54 | * @method Carbon|null getPostModifiedGMT() 55 | * @method setPostMimeType(?string $mimeType) 56 | * @method string|null getPostMimeType() 57 | * @method Post setMenuOrder(?int $order) 58 | * @method int|null getMenuOrder() 59 | * @method Post setPostContentFiltered($content) 60 | * @method string|null getPostContentFiltered() 61 | * @method Post setPostParent(?int $parentId) 62 | * @method int|null getPostParent() 63 | * @method Post setPostAuthor(?int $authorId) 64 | * @method int|null getPostAuthor() 65 | * @method static PostBuilder query() 66 | * 67 | * @property-read User|null $author 68 | * @property-read Collection $metas 69 | * @property-read static|null $parent 70 | * @property-read Collection $comments 71 | */ 72 | class Post extends AbstractModel implements WithMetaModelInterface 73 | { 74 | use HasMetas; 75 | 76 | public const UPDATED_AT = self::MODIFIED; 77 | public const CREATED_AT = self::DATE; 78 | final public const POST_ID = 'ID'; 79 | final public const AUTHOR = 'post_author'; 80 | final public const DATE = 'post_date'; 81 | final public const DATE_GMT = 'post_date_gmt'; 82 | final public const CONTENT = 'post_content'; 83 | final public const TITLE = 'post_title'; 84 | final public const EXCERPT = 'post_excerpt'; 85 | final public const COMMENT_STATUS = 'comment_status'; 86 | final public const STATUS = 'post_status'; 87 | final public const PING_STATUS = 'ping_status'; 88 | final public const PASSWORD = 'post_password'; 89 | final public const POST_NAME = 'post_name'; 90 | final public const TO_PING = 'to_ping'; 91 | final public const PINGED = 'pinged'; 92 | final public const MODIFIED = 'post_modified'; 93 | final public const MODIFIED_GMT = 'post_modified_gmt'; 94 | final public const CONTENT_FILTERED = 'post_content_filtered'; 95 | final public const PARENT = 'post_parent'; 96 | final public const GUID = 'guid'; 97 | final public const MENU_ORDER = 'menu_order'; 98 | final public const TYPE = 'post_type'; 99 | final public const MIME_TYPE = 'post_mime_type'; 100 | final public const COMMENT_COUNT = 'comment_count'; 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | protected $primaryKey = self::POST_ID; 106 | 107 | /** 108 | * @inheritDoc 109 | */ 110 | protected $casts = [ 111 | self::AUTHOR => 'integer', 112 | self::PARENT => 'integer', 113 | self::MENU_ORDER => 'integer', 114 | self::COMMENT_COUNT => 'integer', 115 | self::DATE => 'datetime', 116 | self::MODIFIED => 'datetime', 117 | self::DATE_GMT => 'datetime', 118 | self::MODIFIED_GMT => 'datetime', 119 | ]; 120 | 121 | /** 122 | * @var string 123 | */ 124 | protected $table = 'posts'; 125 | 126 | /** 127 | * @return HasOne 128 | */ 129 | public function author(): HasOne 130 | { 131 | return $this->hasOne(User::class, User::USER_ID, self::AUTHOR); 132 | } 133 | 134 | /** 135 | * @return HasMany 136 | */ 137 | public function comments(): HasMany 138 | { 139 | return $this->hasMany(Comment::class, Comment::POST_ID); 140 | } 141 | 142 | /** 143 | * @return HasOne 144 | */ 145 | public function parent(): HasOne 146 | { 147 | return $this->hasOne(Post::class, Post::POST_ID, self::PARENT); 148 | } 149 | 150 | /** 151 | * @inheritDoc 152 | */ 153 | public function newEloquentBuilder($query): PostBuilder 154 | { 155 | return new PostBuilder($query); 156 | } 157 | 158 | /** 159 | * @param string|null $name 160 | * @return Post|null 161 | */ 162 | public static function findOneByName(?string $name): ?Post 163 | { 164 | /** @var Post|null $model */ 165 | $model = self::query()->firstWhere(self::POST_NAME, $name); 166 | return $model; 167 | } 168 | 169 | /** 170 | * @param string $guid 171 | * @return Post|null 172 | */ 173 | public static function findOneByGuid(string $guid): ?Post 174 | { 175 | /** @var Post|null $model */ 176 | $model = self::query()->firstWhere(self::GUID, $guid); 177 | return $model; 178 | } 179 | 180 | /** 181 | * @return MetaMappingConfig 182 | */ 183 | public function getMetaConfigMapping(): MetaMappingConfig 184 | { 185 | return new MetaMappingConfig(PostMeta::class, PostMeta::POST_ID); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Models/Term.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Dbout\WpOrm\Builders\TermBuilder; 12 | use Dbout\WpOrm\Orm\AbstractModel; 13 | use Illuminate\Database\Eloquent\Relations\HasOne; 14 | 15 | /** 16 | * @method string|null getName() 17 | * @method Term setName(?string $name); 18 | * @method string|null getSlug() 19 | * @method Term setSlug(?string $slug) 20 | * @method int|null getTermGroup() 21 | * @method Term setTermGroup(?int $group) 22 | * @method static TermBuilder query() 23 | * 24 | * @property-read TermTaxonomy|null $termTaxonomy 25 | */ 26 | class Term extends AbstractModel 27 | { 28 | final public const TERM_ID = 'term_id'; 29 | final public const NAME = 'name'; 30 | final public const SLUG = 'slug'; 31 | final public const TERM_GROUP = 'term_group'; 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | protected $table = 'terms'; 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | protected $primaryKey = self::TERM_ID; 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public $timestamps = false; 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | protected $casts = [ 52 | self::TERM_GROUP => 'integer', 53 | ]; 54 | 55 | /** 56 | * @return HasOne 57 | */ 58 | public function termTaxonomy(): HasOne 59 | { 60 | return $this->hasOne( 61 | TermTaxonomy::class, 62 | TermTaxonomy::TERM_ID, 63 | self::TERM_ID 64 | ); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function newEloquentBuilder($query): TermBuilder 71 | { 72 | return new TermBuilder($query); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Models/TermRelationship.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Dbout\WpOrm\Orm\AbstractModel; 12 | 13 | /** 14 | * @method int|null getTermOrder() 15 | * @method TermRelationship setTermOrder(?int $order) 16 | * @method int|null getTermTaxonomyId() 17 | * @method TermRelationship setTermTaxonomyId(?int $id) 18 | * @method int|null getObjectId() 19 | * @method TermRelationship setObjectId(?int $id) 20 | */ 21 | class TermRelationship extends AbstractModel 22 | { 23 | final public const OBJECT_ID = 'object_id'; 24 | final public const TERM_TAXONOMY_ID = 'term_taxonomy_id'; 25 | final public const TERM_ORDER = 'term_order'; 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | protected $primaryKey = self::OBJECT_ID; 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public $timestamps = false; 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | protected $casts = [ 41 | self::TERM_ORDER => 'integer', 42 | self::TERM_TAXONOMY_ID => 'integer', 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /src/Models/TermTaxonomy.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Dbout\WpOrm\Orm\AbstractModel; 12 | 13 | /** 14 | * @method int|null getTermId() 15 | * @method TermTaxonomy setTermId(int $id) 16 | * @method string getTaxonomy() 17 | * @method TermTaxonomy setTaxonomy(string $taxonomy) 18 | * @method string|null getDescription() 19 | * @method TermTaxonomy setDescription(?string $description) 20 | * @method int|null getParent() 21 | * @method TermTaxonomy setParent($parent) 22 | * @method int|null getCount() 23 | * @method TermTaxonomy setCount(int $count) 24 | */ 25 | class TermTaxonomy extends AbstractModel 26 | { 27 | final public const TERM_TAXONOMY_ID = 'term_taxonomy_id'; 28 | final public const TERM_ID = 'term_id'; 29 | final public const TAXONOMY = 'taxonomy'; 30 | final public const DESCRIPTION = 'description'; 31 | final public const PARENT = 'parent'; 32 | final public const COUNT = 'count'; 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public $timestamps = false; 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | protected $table = 'term_taxonomy'; 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | protected $primaryKey = self::TERM_TAXONOMY_ID; 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | protected $casts = [ 53 | self::COUNT => 'integer', 54 | ]; 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | protected $fillable = [ 60 | self::TERM_TAXONOMY_ID, 61 | self::TERM_ID, 62 | self::TAXONOMY, 63 | self::DESCRIPTION, 64 | self::PARENT, 65 | self::COUNT, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /src/Models/User.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Models; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Api\WithMetaModelInterface; 13 | use Dbout\WpOrm\Builders\UserBuilder; 14 | use Dbout\WpOrm\Concerns\HasMetas; 15 | use Dbout\WpOrm\MetaMappingConfig; 16 | use Dbout\WpOrm\Models\Meta\UserMeta; 17 | use Dbout\WpOrm\Orm\AbstractModel; 18 | use Illuminate\Database\Eloquent\Relations\HasMany; 19 | use Illuminate\Support\Collection; 20 | 21 | /** 22 | * @method string|null getUserLogin() 23 | * @method User setUserLogin(string $login) 24 | * @method string|null getUserPass() 25 | * @method User setUserPass(string $password) 26 | * @method string|null getUserNicename() 27 | * @method User setUserNicename(string $nicename) 28 | * @method string|null getUserEmail() 29 | * @method User setUserEmail(string $email) 30 | * @method string|null getUserUrl() 31 | * @method User setUserUrl(?string $url) 32 | * @method Carbon|null getUserRegistered() 33 | * @method User setUserRegistered($date) 34 | * @method string|null getUserActivationKey() 35 | * @method User setUserActivationKey(?string $key) 36 | * @method int getUserStatus() 37 | * @method User setUserStatus(int $status) 38 | * @method string|null getDisplayName() 39 | * @method User setDisplayName(?string $name) 40 | * @method static UserBuilder query() 41 | * 42 | * @property-read Collection $metas 43 | * @property-read Collection $comments 44 | * @property-read Collection $posts 45 | */ 46 | class User extends AbstractModel implements WithMetaModelInterface 47 | { 48 | use HasMetas; 49 | 50 | final public const CREATED_AT = self::REGISTERED; 51 | final public const UPDATED_AT = null; 52 | 53 | final public const USER_ID = 'ID'; 54 | final public const LOGIN = 'user_login'; 55 | final public const PASSWORD = 'user_pass'; 56 | final public const NICE_NAME = 'user_nicename'; 57 | final public const EMAIL = 'user_email'; 58 | final public const URL = 'user_url'; 59 | final public const REGISTERED = 'user_registered'; 60 | final public const ACTIVATION_KEY = 'user_activation_key'; 61 | final public const DISPLAY_NAME = 'display_name'; 62 | final public const STATUS = 'user_status'; 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | protected $table = 'users'; 68 | 69 | /** 70 | * @inheritdoc 71 | */ 72 | protected bool $useBasePrefix = true; 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | protected $casts = [ 78 | self::STATUS => 'integer', 79 | self::REGISTERED => 'datetime', 80 | ]; 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | protected $primaryKey = self::USER_ID; 86 | 87 | /** 88 | * @return HasMany 89 | */ 90 | public function comments(): HasMany 91 | { 92 | return $this->hasMany(Comment::class, Comment::USER_ID); 93 | } 94 | 95 | /** 96 | * @return HasMany 97 | */ 98 | public function posts(): HasMany 99 | { 100 | return $this->hasMany(Post::class, Post::AUTHOR); 101 | } 102 | 103 | /** 104 | * @inheritDoc 105 | */ 106 | public function newEloquentBuilder($query): UserBuilder 107 | { 108 | return new UserBuilder($query); 109 | } 110 | 111 | /** 112 | * @param string $email 113 | * @return self|null 114 | */ 115 | public static function findOneByEmail(string $email): ?self 116 | { 117 | /** @var self|null $result */ 118 | $result = self::query()->firstWhere(self::EMAIL, $email); 119 | return $result; 120 | } 121 | 122 | /** 123 | * @param string $login 124 | * @return self|null 125 | */ 126 | public static function findOneByLogin(string $login): ?self 127 | { 128 | /** @var self|null $result */ 129 | $result = self::query()->firstWhere(self::LOGIN, $login); 130 | return $result; 131 | } 132 | 133 | /** 134 | * @return MetaMappingConfig 135 | */ 136 | public function getMetaConfigMapping(): MetaMappingConfig 137 | { 138 | return new MetaMappingConfig(UserMeta::class, UserMeta::USER_ID); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Orm/AbstractModel.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Orm; 10 | 11 | use Illuminate\Database\Eloquent\Model; 12 | 13 | /** 14 | * @method static int upsert(array $values, array|string $uniqueBy, array|null $update = null) Insert new records or update the existing ones. 15 | * @method static static|null find(int|string $objectId) Retrieve a model by its primary key. 16 | * @method static void truncate() Delete all the model's associated database records, operation will also reset any auto-incrementing IDs on the model's associated table. 17 | * @method static \Illuminate\Database\Eloquent\Builder where($column, $operator = null, $value = null, $boolean = 'and') Add a basic where clause to the query. 18 | * @method static bool insert($query, $bindings = []) Run an insert statement against the database. 19 | */ 20 | abstract class AbstractModel extends Model 21 | { 22 | /** 23 | * @inheritDoc 24 | */ 25 | protected $guarded = []; 26 | 27 | /** 28 | * Indicates if the model should use base prefix for multisite shared tables. 29 | * @var bool 30 | */ 31 | protected bool $useBasePrefix = false; 32 | 33 | /** 34 | * @param array $attributes 35 | */ 36 | public function __construct(array $attributes = []) 37 | { 38 | static::$resolver = new Resolver(); 39 | parent::__construct($attributes); 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function getTable(): ?string 46 | { 47 | /** @var Database $connection */ 48 | $connection = $this->getConnection(); 49 | $prefix = $this->useBasePrefix 50 | ? $connection->getBaseTablePrefix() 51 | : $connection->getTablePrefix(); 52 | 53 | if ($this->table !== null && $this->table !== '') { 54 | return str_starts_with($this->table, $prefix) ? $this->table : $prefix . $this->table; 55 | } 56 | 57 | // Add WordPress table prefix 58 | return $prefix . parent::getTable(); 59 | } 60 | 61 | /** 62 | * @inheritDoc 63 | */ 64 | protected function newBaseQueryBuilder(): Builder 65 | { 66 | $connection = $this->getConnection(); 67 | return new Builder( 68 | $connection, 69 | $connection->getQueryGrammar(), 70 | $connection->getPostProcessor() 71 | ); 72 | } 73 | 74 | /** 75 | * @return int|string|null 76 | */ 77 | public function getId(): null|int|string 78 | { 79 | return $this->{$this->primaryKey}; 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | public function __call($method, $parameters) 86 | { 87 | preg_match('#^(get|set)(.*)#', $method, $matchGetter); 88 | if ($matchGetter === []) { 89 | return parent::__call($method, $parameters); 90 | } 91 | 92 | $type = $matchGetter[1]; 93 | $attribute = $matchGetter[2]; 94 | $attribute = strtolower((string)preg_replace('/(?getAttribute($attribute); 98 | } 99 | 100 | $this->setAttribute($attribute, ...$parameters); 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Orm/Builder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Orm; 10 | 11 | use Illuminate\Database\Query\Builder as EloquentBuilder; 12 | 13 | class Builder extends EloquentBuilder 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/Orm/Query/Grammars/WordPressGrammar.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Orm\Query\Grammars; 10 | 11 | use Illuminate\Database\Query\Grammars\MySqlGrammar; 12 | 13 | class WordPressGrammar extends MySqlGrammar 14 | { 15 | /** 16 | * Fix issue with JSON path 17 | * @see https://github.com/dimitriBouteille/wp-orm/pull/93 18 | * @see https://stackoverflow.com/a/35735594 19 | * @inheritDoc 20 | */ 21 | protected function wrapJsonSelector($value): string 22 | { 23 | [$field, $path] = $this->wrapJsonFieldAndPath($value); 24 | $path = \str_replace('"', '', $path); 25 | return 'json_unquote(json_extract('.$field.$path.'))'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Orm/Query/Processors/WordPressProcessor.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Orm\Query\Processors; 10 | 11 | use Dbout\WpOrm\Orm\Database; 12 | use Illuminate\Database\Query\Builder; 13 | use Illuminate\Database\Query\Processors\Processor; 14 | 15 | class WordPressProcessor extends Processor 16 | { 17 | /** 18 | * @inheritDoc 19 | */ 20 | public function processInsertGetId(Builder $query, $sql, $values, $sequence = null): ?int 21 | { 22 | /** @var Database $co */ 23 | $co = $query->getConnection(); 24 | $co->insert($sql, $values); 25 | 26 | $id = $co->lastInsertId(); 27 | return is_numeric($id) ? (int) $id : $id; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Orm/Resolver.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Orm; 10 | 11 | use Illuminate\Database\ConnectionInterface; 12 | use Illuminate\Database\ConnectionResolverInterface; 13 | 14 | class Resolver implements ConnectionResolverInterface 15 | { 16 | /** 17 | * The default connection name. 18 | * @var string 19 | */ 20 | protected string $default; 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function connection($name = null): ConnectionInterface 26 | { 27 | return Database::getInstance(); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function getDefaultConnection(): string 34 | { 35 | return $this->default ?? ''; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function setDefaultConnection($name): void 42 | { 43 | $this->default = $name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Orm/Schemas/WordPressBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Orm\Schemas; 10 | 11 | use Dbout\WpOrm\Orm\AbstractModel; 12 | use Illuminate\Database\Schema\Grammars\MySqlGrammar; 13 | use Illuminate\Database\Schema\MySqlBuilder; 14 | 15 | class WordPressBuilder extends MySqlBuilder 16 | { 17 | /** 18 | * @var MySqlGrammar 19 | */ 20 | protected $grammar; 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function getColumns($table): array 26 | { 27 | /** 28 | * Never add prefix table because the model::getTable contain the prefix 29 | * @see AbstractModel::getTable() 30 | */ 31 | $results = $this->connection->selectFromWriteConnection( 32 | $this->grammar->compileColumns($this->connection->getDatabaseName(), $table) 33 | ); 34 | 35 | return $this->connection->getPostProcessor()->processColumns($results); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Scopes/CustomModelTypeScope.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Scopes; 10 | 11 | use Dbout\WpOrm\Api\CustomModelTypeInterface; 12 | use Dbout\WpOrm\Exceptions\WpOrmException; 13 | use Illuminate\Database\Eloquent\Builder; 14 | use Illuminate\Database\Eloquent\Model; 15 | use Illuminate\Database\Eloquent\Scope; 16 | 17 | class CustomModelTypeScope implements Scope 18 | { 19 | /** 20 | * @inheritDoc 21 | * @throws WpOrmException 22 | */ 23 | public function apply(Builder $builder, Model $model): void 24 | { 25 | if (!$model instanceof CustomModelTypeInterface) { 26 | throw new WpOrmException(sprintf( 27 | 'The object %s must be implement %s.', 28 | get_class($model), 29 | CustomModelTypeInterface::class 30 | )); 31 | } 32 | 33 | $builder->where($model->getCustomTypeColumn(), $model->getCustomTypeCode()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Taps/Attachment/IsMimeTypeTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Attachment; 10 | 11 | use Dbout\WpOrm\Builders\PostBuilder; 12 | use Dbout\WpOrm\Models\Post; 13 | 14 | readonly class IsMimeTypeTap 15 | { 16 | /** 17 | * @param string $mimeType 18 | */ 19 | public function __construct( 20 | protected string $mimeType 21 | ) { 22 | } 23 | 24 | /** 25 | * @param PostBuilder $builder 26 | * @return void 27 | */ 28 | public function __invoke(PostBuilder $builder): void 29 | { 30 | $builder->where(Post::MIME_TYPE, $this->mimeType); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Taps/Comment/IsApprovedTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Comment; 10 | 11 | use Dbout\WpOrm\Builders\CommentBuilder; 12 | use Dbout\WpOrm\Models\Comment; 13 | 14 | readonly class IsApprovedTap 15 | { 16 | /** 17 | * @param bool $isApproved 18 | */ 19 | public function __construct( 20 | protected bool $isApproved = true 21 | ) { 22 | } 23 | 24 | /** 25 | * @param CommentBuilder $builder 26 | * @return void 27 | */ 28 | public function __invoke(CommentBuilder $builder): void 29 | { 30 | $builder->where(Comment::APPROVED, $this->isApproved); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Taps/Comment/IsCommentTypeTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Comment; 10 | 11 | use Dbout\WpOrm\Builders\CommentBuilder; 12 | use Dbout\WpOrm\Models\Comment; 13 | 14 | readonly class IsCommentTypeTap 15 | { 16 | /** 17 | * @param string $commentType 18 | */ 19 | public function __construct( 20 | protected string $commentType 21 | ) { 22 | } 23 | 24 | /** 25 | * @param CommentBuilder $builder 26 | * @return void 27 | */ 28 | public function __invoke(CommentBuilder $builder): void 29 | { 30 | $builder->where(Comment::TYPE, $this->commentType); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Taps/Comment/IsUserTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Comment; 10 | 11 | use Dbout\WpOrm\Builders\CommentBuilder; 12 | use Dbout\WpOrm\Models\Comment; 13 | use Dbout\WpOrm\Models\User; 14 | 15 | readonly class IsUserTap 16 | { 17 | /** 18 | * @param int|User $user 19 | */ 20 | public function __construct( 21 | protected int|User $user 22 | ) { 23 | } 24 | 25 | /** 26 | * @param CommentBuilder $builder 27 | * @return void 28 | */ 29 | public function __invoke(CommentBuilder $builder): void 30 | { 31 | $user = $this->user; 32 | if ($user instanceof User) { 33 | $user = $user->getId(); 34 | } 35 | 36 | $builder->where(Comment::USER_ID, $user); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Taps/Option/IsAutoloadTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Option; 10 | 11 | use Dbout\WpOrm\Builders\OptionBuilder; 12 | use Dbout\WpOrm\Enums\YesNo; 13 | use Dbout\WpOrm\Models\Option; 14 | 15 | readonly class IsAutoloadTap 16 | { 17 | public function __construct( 18 | protected bool|YesNo $autoload = YesNo::Yes 19 | ) { 20 | } 21 | 22 | /** 23 | * @param OptionBuilder $builder 24 | * @return void 25 | */ 26 | public function __invoke(OptionBuilder $builder): void 27 | { 28 | $autoload = $this->autoload; 29 | if (is_bool($autoload)) { 30 | $autoload = $autoload ? YesNo::Yes : YesNo::No; 31 | } 32 | 33 | $builder->where(Option::AUTOLOAD, $autoload->value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Taps/Post/IsAuthorTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Post; 10 | 11 | use Dbout\WpOrm\Builders\PostBuilder; 12 | use Dbout\WpOrm\Models\Post; 13 | use Dbout\WpOrm\Models\User; 14 | 15 | readonly class IsAuthorTap 16 | { 17 | /** 18 | * @param int|User $author 19 | */ 20 | public function __construct( 21 | protected int|User $author 22 | ) { 23 | } 24 | 25 | /** 26 | * @param PostBuilder $builder 27 | * @return void 28 | */ 29 | public function __invoke(PostBuilder $builder): void 30 | { 31 | $author = $this->author; 32 | if ($author instanceof User) { 33 | $author = $author->getId(); 34 | } 35 | 36 | $builder->where(Post::AUTHOR, $author); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Taps/Post/IsPingStatusTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Post; 10 | 11 | use Dbout\WpOrm\Builders\PostBuilder; 12 | use Dbout\WpOrm\Enums\PingStatus; 13 | use Dbout\WpOrm\Models\Post; 14 | 15 | readonly class IsPingStatusTap 16 | { 17 | /** 18 | * @param string|PingStatus $status 19 | */ 20 | public function __construct( 21 | protected string|PingStatus $status 22 | ) { 23 | } 24 | 25 | /** 26 | * @param PostBuilder $builder 27 | * @return void 28 | */ 29 | public function __invoke(PostBuilder $builder): void 30 | { 31 | $status = $this->status; 32 | if ($status instanceof PingStatus) { 33 | $status = $status->value; 34 | } 35 | 36 | $builder->where(Post::PING_STATUS, $status); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Taps/Post/IsPostTypeTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Post; 10 | 11 | use Dbout\WpOrm\Builders\PostBuilder; 12 | use Dbout\WpOrm\Models\Post; 13 | 14 | readonly class IsPostTypeTap 15 | { 16 | /** 17 | * @param string $postType 18 | */ 19 | public function __construct( 20 | protected string $postType 21 | ) { 22 | } 23 | 24 | /** 25 | * @param PostBuilder $builder 26 | * @return void 27 | */ 28 | public function __invoke(PostBuilder $builder): void 29 | { 30 | $builder->where(Post::TYPE, $this->postType); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Taps/Post/IsStatusTap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Taps\Post; 10 | 11 | use Dbout\WpOrm\Builders\PostBuilder; 12 | use Dbout\WpOrm\Enums\PostStatus; 13 | use Dbout\WpOrm\Models\Post; 14 | 15 | readonly class IsStatusTap 16 | { 17 | /** 18 | * @param string|PostStatus $status 19 | */ 20 | public function __construct( 21 | protected string|PostStatus $status 22 | ) { 23 | } 24 | 25 | /** 26 | * @param PostBuilder $builder 27 | * @return void 28 | */ 29 | public function __invoke(PostBuilder $builder): void 30 | { 31 | $status = $this->status; 32 | if ($status instanceof PostStatus) { 33 | $status = $status->value; 34 | } 35 | 36 | $builder->where(Post::STATUS, $status); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | if (!function_exists('event')) { 10 | /** 11 | * Dispatch an event and call the listeners. 12 | * 13 | * @param ...$args 14 | * @return void 15 | */ 16 | function event(...$args): void 17 | { 18 | /** 19 | * By default, event function comes from Laravel framework. At the moment this function is disabled 20 | * because it cannot be used with this library since there is no event manager. 21 | * @see https://github.com/laravel/framework/blob/11.x/src/Illuminate/Foundation/helpers.php#L462 22 | */ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit; 10 | 11 | class Bootstrap 12 | { 13 | private static ?self $instance = null; 14 | 15 | protected string $wpDirectory = ''; 16 | 17 | public function __construct() 18 | { 19 | $this->wpDirectory = __DIR__ . '/../../web/wordpress'; 20 | $this->initConstants(); 21 | $this->loadFiles(); 22 | } 23 | 24 | /** 25 | * @return self 26 | */ 27 | public static function run(): self 28 | { 29 | if (!self::$instance instanceof Bootstrap) { 30 | self::$instance = new self(); 31 | } 32 | 33 | return self::$instance; 34 | } 35 | 36 | /** 37 | * @return void 38 | */ 39 | protected function initConstants(): void 40 | { 41 | define('ABSPATH', sprintf('%s/', $this->wpDirectory)); 42 | define('WP_DEBUG', false); 43 | define('WP_CONTENT_DIR', '/'); 44 | define('WP_DEBUG_LOG', false); 45 | define('WP_PLUGIN_DIR', './'); 46 | define('WPMU_PLUGIN_DIR', './'); 47 | define('EMPTY_TRASH_DAYS', 30 * 86400); 48 | define('SCRIPT_DEBUG', false); 49 | define('WP_LANG_DIR', './'); 50 | define('WPINC', 'wp-includes'); 51 | } 52 | 53 | /** 54 | * @return void 55 | */ 56 | protected function loadFiles(): void 57 | { 58 | $paths = [ 59 | 'load.php', 60 | 'functions.php', 61 | 'plugin.php', 62 | 'class-wpdb.php', 63 | 'class-wp-error.php', 64 | ]; 65 | 66 | foreach ($paths as $path) { 67 | require sprintf('%s/wp-includes/%s', $this->wpDirectory, $path); 68 | } 69 | } 70 | } 71 | 72 | Bootstrap::run(); 73 | -------------------------------------------------------------------------------- /tests/Unit/Concerns/HasMetasTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit\Concerns; 10 | 11 | use Dbout\WpOrm\Models\Post; 12 | use PHPUnit\Framework\Attributes\CoversClass; 13 | use PHPUnit\Framework\Attributes\CoversMethod; 14 | use PHPUnit\Framework\TestCase; 15 | 16 | #[CoversClass(Post::class)] 17 | #[CoversMethod(Post::class, 'metaHasCast')] 18 | #[CoversMethod(Post::class, 'getMetaCasts')] 19 | class HasMetasTest extends TestCase 20 | { 21 | /** 22 | * @return void 23 | */ 24 | public function testMetaHasCastWithProperty(): void 25 | { 26 | $model = new class () extends Post { 27 | protected array $metaCasts = [ 28 | 'my_meta' => 'int', 29 | ]; 30 | }; 31 | 32 | $this->assertTrue($model->metaHasCast('my_meta')); 33 | $this->assertTrue($model->metaHasCast('my_meta', 'int')); 34 | $this->assertFalse($model->metaHasCast('my_meta', 'boolean')); 35 | $this->assertFalse($model->metaHasCast('custom-meta')); 36 | } 37 | 38 | /** 39 | * @return void 40 | */ 41 | public function testMetaHasCastWithMetaCastsFunction(): void 42 | { 43 | $model = new class () extends Post { 44 | /** 45 | * @inheritDoc 46 | */ 47 | protected function metaCasts(): array 48 | { 49 | return [ 50 | 'my_meta' => 'int', 51 | ]; 52 | } 53 | }; 54 | 55 | $this->assertTrue($model->metaHasCast('my_meta')); 56 | $this->assertTrue($model->metaHasCast('my_meta', 'int')); 57 | $this->assertFalse($model->metaHasCast('my_meta', 'boolean')); 58 | $this->assertFalse($model->metaHasCast('custom-meta')); 59 | } 60 | 61 | /** 62 | * @return void 63 | */ 64 | public function testGetMetaCasts(): void 65 | { 66 | $model = new class () extends Post { 67 | protected array $metaCasts = [ 68 | 'custom-meta' => 'int', 69 | ]; 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | protected function metaCasts(): array 75 | { 76 | return [ 77 | 'my-meta' => 'string', 78 | ]; 79 | } 80 | }; 81 | 82 | $this->assertEquals([ 83 | 'custom-meta' => 'int', 84 | 'my-meta' => 'string', 85 | ], $model->getMetaCasts()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Unit/Models/CommentTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit\Models; 10 | 11 | use Dbout\WpOrm\Models\Comment; 12 | use PHPUnit\Framework\Attributes\CoversClass; 13 | use PHPUnit\Framework\Attributes\CoversMethod; 14 | use PHPUnit\Framework\TestCase; 15 | 16 | #[CoversClass(Comment::class)] 17 | #[CoversMethod(Comment::class, 'getCommentAuthorIpAttribute')] 18 | #[CoversMethod(Comment::class, 'setCommentAuthorIpAttribute')] 19 | #[CoversMethod(Comment::class, 'getCommentPostIdAttribute')] 20 | #[CoversMethod(Comment::class, 'setCommentPostIdAttribute')] 21 | class CommentTest extends TestCase 22 | { 23 | /** 24 | * @return void 25 | */ 26 | public function testCommentAuthorIP(): void 27 | { 28 | $comment = new Comment(); 29 | $comment->setCommentAuthorIP('127.0.0.1'); 30 | $this->assertEquals('127.0.0.1', $comment->getCommentAuthorIP()); 31 | $this->assertEquals('127.0.0.1', $comment->getAttribute('comment_author_IP')); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function testCommentPostID(): void 38 | { 39 | $comment = new Comment(); 40 | $comment->setCommentPostID(1525); 41 | $this->assertEquals(1525, $comment->getCommentPostID()); 42 | $this->assertEquals(1525, $comment->getAttribute('comment_post_ID')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/Models/CustomCommentTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit\Models; 10 | 11 | use Dbout\WpOrm\Exceptions\CannotOverrideCustomTypeException; 12 | use Dbout\WpOrm\Exceptions\NotAllowedException; 13 | use Dbout\WpOrm\Models\CustomComment; 14 | use PHPUnit\Framework\Attributes\CoversClass; 15 | use PHPUnit\Framework\Attributes\CoversMethod; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | #[CoversClass(CustomComment::class)] 19 | #[CoversMethod(CustomComment::class, 'setCommentType')] 20 | #[CoversMethod(CustomComment::class, 'getCommentType')] 21 | #[CoversMethod(CustomComment::class, 'setAttribute')] 22 | class CustomCommentTest extends TestCase 23 | { 24 | /** 25 | * @throws NotAllowedException 26 | * @return never 27 | */ 28 | public function testSetCommentTypeTypeException(): never 29 | { 30 | $model = new class () extends CustomComment { 31 | protected string $_type = 'woocommerce'; 32 | }; 33 | 34 | $model = new $model(); 35 | $this->expectException(CannotOverrideCustomTypeException::class); 36 | $this->expectExceptionMessage('You cannot override type for this object. Current type [woocommerce]'); 37 | $model->setCommentType('my_type'); 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | public function testSetCommentTypeInConstructor(): void 44 | { 45 | $model = new class () extends CustomComment { 46 | protected string $_type = 'woocommerce'; 47 | }; 48 | 49 | $model = new $model([ 50 | 'comment_type' => 'application', 51 | ]); 52 | 53 | $this->assertEquals('woocommerce', $model->getCommentType()); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function testSetCommentTypeWithSetAttribute(): void 60 | { 61 | $model = new class () extends CustomComment { 62 | protected string $_type = 'woocommerce'; 63 | }; 64 | 65 | $model = new $model(); 66 | $model->setAttribute('comment_type', 'application'); 67 | $this->assertEquals('woocommerce', $model->getCommentType()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Unit/Models/CustomPostTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit\Models; 10 | 11 | use Dbout\WpOrm\Exceptions\CannotOverrideCustomTypeException; 12 | use Dbout\WpOrm\Exceptions\NotAllowedException; 13 | use Dbout\WpOrm\Models\CustomPost; 14 | use PHPUnit\Framework\Attributes\CoversClass; 15 | use PHPUnit\Framework\Attributes\CoversMethod; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | #[CoversClass(CustomPost::class)] 19 | #[CoversMethod(CustomPost::class, 'setPostType')] 20 | #[CoversMethod(CustomPost::class, 'getPostType')] 21 | #[CoversMethod(CustomPost::class, 'setAttribute')] 22 | class CustomPostTest extends TestCase 23 | { 24 | /** 25 | * @throws NotAllowedException 26 | * @return never 27 | */ 28 | public function testSetPostTypeException(): never 29 | { 30 | $model = new class () extends CustomPost { 31 | protected string $_type = 'product'; 32 | }; 33 | 34 | $this->expectException(CannotOverrideCustomTypeException::class); 35 | $this->expectExceptionMessage('You cannot override type for this object. Current type [product]'); 36 | $model->setPostType('my_type'); 37 | } 38 | 39 | /** 40 | * @return void 41 | */ 42 | public function testSetPostTypeInConstructor(): void 43 | { 44 | $model = new class () extends CustomPost { 45 | protected string $_type = 'product'; 46 | }; 47 | 48 | $model = new $model([ 49 | 'post_type' => 'my_type', 50 | ]); 51 | 52 | $this->assertEquals('product', $model->getPostType()); 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | public function testSetPostTypeWithSetAttribute(): void 59 | { 60 | $model = new class () extends CustomPost { 61 | protected string $_type = 'product'; 62 | }; 63 | 64 | $model = new $model(); 65 | $model->setAttribute('post_type', 'architect'); 66 | $this->assertEquals('product', $model->getPostType()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/Orm/DatabaseTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit\Orm; 10 | 11 | use Dbout\WpOrm\Orm\Database; 12 | use PHPUnit\Framework\Attributes\CoversClass; 13 | use PHPUnit\Framework\Attributes\CoversMethod; 14 | use PHPUnit\Framework\TestCase; 15 | 16 | #[CoversClass(Database::class)] 17 | #[CoversMethod(Database::class, 'getInstance')] 18 | class DatabaseTest extends TestCase 19 | { 20 | /** 21 | * @return void 22 | */ 23 | public function testInvalidWPInstance(): void 24 | { 25 | $this->expectException(\Exception::class); 26 | $this->expectExceptionMessage('The global variable $wpdb must be instance of \wpdb.'); 27 | Database::getInstance(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/Scopes/CustomModelTypeScopeTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\Unit\Scopes; 10 | 11 | use Dbout\WpOrm\Builders\OptionBuilder; 12 | use Dbout\WpOrm\Builders\PostBuilder; 13 | use Dbout\WpOrm\Exceptions\WpOrmException; 14 | use Dbout\WpOrm\Models\Attachment; 15 | use Dbout\WpOrm\Models\Option; 16 | use Dbout\WpOrm\Scopes\CustomModelTypeScope; 17 | use PHPUnit\Framework\Attributes\CoversClass; 18 | use PHPUnit\Framework\Attributes\CoversMethod; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | #[CoversClass(CustomModelTypeScope::class)] 22 | #[CoversMethod(CustomModelTypeScope::class, 'apply')] 23 | class CustomModelTypeScopeTest extends TestCase 24 | { 25 | private CustomModelTypeScope $subject; 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | protected function setUp(): void 31 | { 32 | $this->subject = new CustomModelTypeScope(); 33 | } 34 | 35 | /** 36 | * @throws WpOrmException 37 | * @throws \PHPUnit\Framework\MockObject\Exception 38 | * @return void 39 | */ 40 | public function testWithInvalidModel(): void 41 | { 42 | $model = new Option(); 43 | $builder = $this->createMock(OptionBuilder::class); 44 | $this->expectException(WpOrmException::class); 45 | $this->subject->apply($builder, $model); 46 | } 47 | 48 | /** 49 | * @throws WpOrmException 50 | * @throws \PHPUnit\Framework\MockObject\Exception 51 | * @return void 52 | */ 53 | public function testBuilderContainFilter(): void 54 | { 55 | $model = new Attachment(); 56 | $query = new \Illuminate\Database\Query\Builder( 57 | $this->createMock(\Illuminate\Database\MySqlConnection::class), 58 | new \Illuminate\Database\Query\Grammars\Grammar(), 59 | new \Illuminate\Database\Query\Processors\Processor() 60 | ); 61 | $builder = new PostBuilder($query); 62 | $this->subject->apply($builder, $model); 63 | 64 | $this->assertEquals('select * where "post_type" = ?', $builder->toSql()); 65 | $this->assertEquals([ 66 | 'attachment', 67 | ], $builder->getBindings()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/WordPress/Bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress; 10 | 11 | class Bootstrap 12 | { 13 | private const VENDOR_DIR = __DIR__ . '/../../vendor'; 14 | private static ?self $instance = null; 15 | 16 | /** 17 | * @var string|null 18 | */ 19 | protected ?string $wpTestsDir = null; 20 | 21 | 22 | public function __construct() 23 | { 24 | $this->wpTestsDir = $this->getPathToWpTestDir(); 25 | 26 | /** 27 | * Load WordPress 28 | */ 29 | $this->initBootstrap(); 30 | 31 | /** 32 | * This function has to be called _last_, after the WP test bootstrap to make sure it registers 33 | * itself in FRONT of the Composer autoload (which also prepends itself to the autoload queue). 34 | */ 35 | $this->loadComposerAutoloader(); 36 | } 37 | 38 | /** 39 | * @param string $path 40 | * @return string 41 | */ 42 | private function normalizePath(string $path): string 43 | { 44 | return \str_replace('\\', '/', $path); 45 | } 46 | 47 | /** 48 | * @return string|null 49 | */ 50 | protected function getPathToWpTestDir(): ?string 51 | { 52 | if (\getenv('WP_TESTS_DIR') !== false) { 53 | $testsDir = \getenv('WP_TESTS_DIR'); 54 | $testsDir = \realpath($testsDir); 55 | 56 | if ($testsDir !== false) { 57 | $testsDir = $this->normalizePath($testsDir) . '/'; 58 | if (\is_dir($testsDir) === true 59 | && @\file_exists($testsDir . 'includes/bootstrap.php') 60 | ) { 61 | return $testsDir; 62 | } 63 | } 64 | 65 | unset($testsDir); 66 | } 67 | 68 | if (\getenv('WP_DEVELOP_DIR') !== false) { 69 | $devDir = \getenv('WP_DEVELOP_DIR'); 70 | $devDir = \realpath($devDir); 71 | if ($devDir !== false) { 72 | $devDir = $this->normalizePath($devDir) . '/'; 73 | if (\is_dir($devDir) === true 74 | && @\file_exists($devDir . 'tests/phpunit/includes/bootstrap.php') 75 | ) { 76 | return $devDir . 'tests/phpunit/'; 77 | } 78 | } 79 | 80 | unset($devDir); 81 | } 82 | 83 | /** 84 | * Last resort: see if this is a typical WP-CLI scaffold command set-up where a subset of 85 | * the WP test files have been put in the system temp directory. 86 | */ 87 | $testsDir = \sys_get_temp_dir() . '/wordpress-tests-lib'; 88 | $testsDir = \realpath($testsDir); 89 | if ($testsDir !== false) { 90 | $testsDir = $this->normalizePath($testsDir) . '/'; 91 | if (\is_dir($testsDir) === true 92 | && @\file_exists($testsDir . 'includes/bootstrap.php') 93 | ) { 94 | return $testsDir; 95 | } 96 | } 97 | 98 | return null; 99 | } 100 | 101 | /** 102 | * Verifies whether the Composer autoload file exists. 103 | * 104 | * @return void 105 | */ 106 | protected function checkComposerInstalled(): void 107 | { 108 | $path = sprintf('%s/autoload.php', self::VENDOR_DIR); 109 | if (!@file_exists($path)) { 110 | echo \PHP_EOL, 'ERROR: Run `composer install` or `composer update -W` to install the dependencies', 111 | ' and generate the autoload files before running the unit tests.', \PHP_EOL; 112 | exit(1); 113 | } 114 | } 115 | 116 | /** 117 | * Load the Composer autoload file. 118 | * 119 | * @return void 120 | */ 121 | protected function loadComposerAutoloader(): void 122 | { 123 | $this->checkComposerInstalled(); 124 | $path = __DIR__ . '/../../vendor/autoload.php'; 125 | 126 | require_once sprintf('%s/autoload.php', self::VENDOR_DIR); 127 | } 128 | 129 | /** 130 | * Loads the WordPress native test bootstrap file to set up the environment. 131 | * 132 | * @return void 133 | */ 134 | protected function initBootstrap(): void 135 | { 136 | if ($this->wpTestsDir === null) { 137 | echo \PHP_EOL, 'ERROR: The WordPress native unit test bootstrap file could not be found. Please set either the WP_TESTS_DIR or the WP_DEVELOP_DIR environment variable, either in your OS or in a custom phpunit.xml file.', \PHP_EOL; 138 | exit(1); 139 | } 140 | 141 | $this->checkComposerInstalled(); 142 | 143 | /** 144 | * Load PHPUnit Polyfills for the WP testing suite. 145 | * @see https://github.com/WordPress/wordpress-develop/pull/1563/ 146 | */ 147 | require_once sprintf('%s/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php', self::VENDOR_DIR); 148 | 149 | /** 150 | * We can safely load the bootstrap - already verifies it exists. 151 | * Load the WP testing environment. 152 | */ 153 | require_once sprintf('%s/includes/bootstrap.php', rtrim($this->wpTestsDir, '/')); 154 | } 155 | 156 | /** 157 | * Loads the WP native integration test bootstrap and register a custom autoloader. 158 | * 159 | * @return self 160 | */ 161 | public static function run(): self 162 | { 163 | if (!self::$instance instanceof Bootstrap) { 164 | self::$instance = new self(); 165 | } 166 | 167 | return self::$instance; 168 | } 169 | } 170 | 171 | Bootstrap::run(); 172 | -------------------------------------------------------------------------------- /tests/WordPress/Concerns/HasMetasTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Concerns; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Concerns\HasMetas; 13 | use Dbout\WpOrm\Enums\YesNo; 14 | use Dbout\WpOrm\Models\Meta\AbstractMeta; 15 | use Dbout\WpOrm\Models\Meta\PostMeta; 16 | use Dbout\WpOrm\Models\Post; 17 | use Dbout\WpOrm\Tests\WordPress\TestCase; 18 | use Illuminate\Events\Dispatcher; 19 | 20 | class HasMetasTest extends TestCase 21 | { 22 | /** 23 | * @return void 24 | * @covers HasMetas::getMeta 25 | * @uses Post 26 | */ 27 | public function testGetMeta(): void 28 | { 29 | $model = new Post(); 30 | $model->setPostTitle(__FUNCTION__); 31 | $model->save(); 32 | $createMeta = $model->setMeta('author', 'Norman FOSTER'); 33 | 34 | $meta = $model->getMeta('author'); 35 | $this->assertLastQueryEquals($this->getQueryGetMeta($model->getId(), 'author')); 36 | $this->assertInstanceOf(AbstractMeta::class, $meta); 37 | $this->assertEquals($createMeta->getId(), $meta->getId()); 38 | $this->assertEquals($createMeta->getValue(), $meta->getValue()); 39 | $this->assertEquals('Norman FOSTER', $meta->getValue()); 40 | $this->assertEquals('author', $meta->getKey()); 41 | } 42 | 43 | /** 44 | * @return void 45 | * @covers HasMetas::setMeta 46 | * @uses Post 47 | */ 48 | public function testSetMeta(): void 49 | { 50 | $model = new Post(); 51 | $model->setPostTitle(__FUNCTION__); 52 | $model->save(); 53 | $meta = $model->setMeta('build-by', 'John D.'); 54 | 55 | $this->assertEquals('John D.', get_post_meta($model->getId(), 'build-by', true)); 56 | $this->assertInstanceOf(PostMeta::class, $meta); 57 | 58 | $loadedMeta = $model->getMeta('build-by'); 59 | $this->assertEquals($meta->getId(), $loadedMeta->getId()); 60 | } 61 | 62 | /** 63 | * @return void 64 | * @covers HasMetas::hasMeta 65 | * @uses Post 66 | */ 67 | public function testHasMeta(): void 68 | { 69 | $model = new Post(); 70 | $model->setPostTitle(__FUNCTION__); 71 | $model->save(); 72 | 73 | $model->setMeta('birthday-date', '17/09/1900'); 74 | $this->assertTrue($model->hasMeta('birthday-date')); 75 | 76 | $wpMetaId = add_post_meta($model->getId(), 'birthday-place', 'France'); 77 | $this->assertTrue($model->hasMeta('birthday-place')); 78 | $this->assertEquals('France', $model->getMetaValue('birthday-place')); 79 | $this->assertEquals($wpMetaId, $model->getMeta('birthday-place')?->getId()); 80 | } 81 | 82 | /** 83 | * @return void 84 | * @covers HasMetas::getMetaValue 85 | * @uses Post 86 | */ 87 | public function testGetMetaValueWithoutCast(): void 88 | { 89 | $model = new Post(); 90 | $model->setPostTitle(__FUNCTION__); 91 | 92 | $model->save(); 93 | $model->setMeta('build-by', 'John D.'); 94 | 95 | add_post_meta($model->getId(), 'place', 'Lyon, France'); 96 | $this->assertEquals('Lyon, France', $model->getMetaValue('place')); 97 | $this->assertLastQueryEquals($this->getQueryGetMeta($model->getId(), 'place')); 98 | } 99 | 100 | /** 101 | * @return void 102 | * @covers HasMetas::getMetaValue 103 | * @uses Post 104 | */ 105 | public function testGetMetaValueWithGenericCasts(): void 106 | { 107 | $object = new class () extends Post { 108 | protected array $metaCasts = [ 109 | 'age' => 'int', 110 | 'year' => 'integer', 111 | 'is_active' => 'bool', 112 | 'subscribed' => 'boolean', 113 | 'data' => 'json', 114 | ]; 115 | }; 116 | 117 | $model = new $object(); 118 | $model->setPostTitle(__FUNCTION__); 119 | $model->save(); 120 | $model->setMeta('age', '18'); 121 | $model->setMeta('year', '2024'); 122 | $model->setMeta('is_active', '1'); 123 | $model->setMeta('subscribed', '0'); 124 | $model->setMeta('data', '{"firstname":"John","lastname":"Doe"}'); 125 | 126 | $this->assertEquals(18, $model->getMetaValue('age')); 127 | $this->assertEquals(2024, $model->getMetaValue('year')); 128 | $this->assertTrue($model->getMetaValue('is_active')); 129 | $this->assertFalse($model->getMetaValue('subscribed')); 130 | $this->assertEquals(['firstname' => 'John', 'lastname' => 'Doe'], $model->getMetaValue('data')); 131 | } 132 | 133 | /** 134 | * @return void 135 | * @covers HasMetas::getMetaValue 136 | * @uses Post 137 | */ 138 | public function testGetMetaValueWithEnumCasts(): void 139 | { 140 | $object = new class () extends Post { 141 | protected array $metaCasts = [ 142 | 'active' => YesNo::class, 143 | ]; 144 | }; 145 | 146 | $model = new $object(); 147 | $model->setPostTitle(__FUNCTION__); 148 | $model->save(); 149 | $model->setMeta('active', 'yes'); 150 | 151 | /** @var YesNo $value */ 152 | $value = $model->getMetaValue('active'); 153 | 154 | $this->assertInstanceOf(YesNo::class, $value); 155 | $this->assertEquals('yes', $value->value); 156 | } 157 | 158 | /** 159 | * @return void 160 | * @covers HasMetas::getMetaValue 161 | * @uses Post 162 | */ 163 | public function testGetMetaValueWithDatetimeCasts(): void 164 | { 165 | $object = new class () extends Post { 166 | protected array $metaCasts = [ 167 | 'created_at' => 'datetime', 168 | 'uploaded_at' => 'date', 169 | ]; 170 | }; 171 | 172 | $model = new $object(); 173 | $model->setPostTitle(__FUNCTION__); 174 | $model->save(); 175 | $model->setMeta('created_at', '2022-09-08 07:30:05'); 176 | $model->setMeta('uploaded_at', '2024-10-08 10:25:35'); 177 | 178 | /** @var Carbon $date */ 179 | $date = $model->getMetaValue('created_at'); 180 | $this->assertInstanceOf(Carbon::class, $date); 181 | $this->assertEquals('2022-09-08 07:30:05', $date->format('Y-m-d H:i:s')); 182 | 183 | /** @var Carbon $date */ 184 | $date = $model->getMetaValue('uploaded_at'); 185 | $this->assertInstanceOf(Carbon::class, $date); 186 | $this->assertEquals('2024-10-08 00:00:00', $date->format('Y-m-d H:i:s'), 'The time must be reset to 00:00:00.'); 187 | } 188 | 189 | /** 190 | * @return void 191 | * @covers HasMetas::getMetaValue 192 | * @uses Post 193 | */ 194 | public function testGetMetaValueWithInvalidCasts(): void 195 | { 196 | $object = new class () extends Post { 197 | protected array $metaCasts = [ 198 | 'my_meta' => 'boolean_', 199 | ]; 200 | }; 201 | 202 | $model = new $object(); 203 | $model->setPostTitle(__FUNCTION__); 204 | $model->save(); 205 | $model->setMeta('my_meta', 'yes'); 206 | 207 | $this->assertEquals('yes', $model->getMetaValue('my_meta')); 208 | } 209 | 210 | /** 211 | * @return void 212 | * @covers HasMetas::deleteMeta 213 | * @uses Post 214 | */ 215 | public function testDeleteMeta(): void 216 | { 217 | $model = new Post(); 218 | $model->setPostTitle(__FUNCTION__); 219 | $model->save(); 220 | 221 | $model->setMeta('architect-name', 'Norman F.'); 222 | 223 | $this->assertEquals(1, $model->deleteMeta('architect-name'), 'The function must delete only one line.'); 224 | $this->assertLastQueryEquals(sprintf( 225 | "delete from `%1\$s` where `%1\$s`.`post_id` = %2\$d and `%1\$s`.`post_id` is not null and `meta_key` = 'architect-name'", 226 | '#TABLE_PREFIX#postmeta', 227 | $model->getId() 228 | )); 229 | 230 | $this->assertFalse($model->hasMeta('architect-name'), 'The meta must no longer exist.'); 231 | } 232 | 233 | /** 234 | * @return void 235 | * @covers HasMetas::deleteMeta 236 | * @uses Post 237 | */ 238 | public function testDeleteUndefinedMeta(): void 239 | { 240 | $model = new Post(); 241 | $model->setPostTitle(__FUNCTION__); 242 | $model->save(); 243 | 244 | $model->setMeta('architect-name', 'Norman F.'); 245 | 246 | $this->assertEquals(0, $model->deleteMeta('fake-meta')); 247 | 248 | $this->assertLastQueryEquals(sprintf( 249 | "delete from `%1\$s` where `%1\$s`.`post_id` = %2\$d and `%1\$s`.`post_id` is not null and `meta_key` = 'fake-meta'", 250 | '#TABLE_PREFIX#postmeta', 251 | $model->getId() 252 | )); 253 | } 254 | 255 | /** 256 | * @return void 257 | * @covers HasMetas::setMeta 258 | * @covers HasMetas::hasMeta 259 | * @covers HasMetas::getMeta 260 | * @uses Post 261 | */ 262 | public function testSaveNewModelWithMetas(): void 263 | { 264 | $metaValue = '31-10-1950'; 265 | $metaKey = 'birthday-date'; 266 | 267 | $object = new class () extends Post { 268 | protected static function boot() 269 | { 270 | static::setEventDispatcher(new Dispatcher()); 271 | parent::boot(); 272 | } 273 | }; 274 | 275 | $model = new $object(); 276 | $model->setPostTitle('Zaha Hadid projects'); 277 | $model->setMeta($metaKey, $metaValue); 278 | $model->save(); 279 | 280 | $this->assertEquals($metaValue, get_post_meta($model->getId(), $metaKey, true)); 281 | $this->assertInstanceOf(PostMeta::class, $model->getMeta($metaKey)); 282 | $this->assertTrue($model->hasMeta($metaKey)); 283 | } 284 | 285 | /** 286 | * @param int $postId 287 | * @param string $metaKey 288 | * @return string 289 | */ 290 | private function getQueryGetMeta(int $postId, string $metaKey): string 291 | { 292 | return sprintf( 293 | "select * from `%1\$s` where `%1\$s`.`post_id` = %2\$d and `%1\$s`.`post_id` is not null and `meta_key` = '%3\$s' limit 1", 294 | '#TABLE_PREFIX#postmeta', 295 | $postId, 296 | $metaKey 297 | ); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /tests/WordPress/Concerns/PrunableTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Concerns; 10 | 11 | use Carbon\Carbon; 12 | use Dbout\WpOrm\Orm\AbstractModel; 13 | use Dbout\WpOrm\Orm\Database; 14 | use Dbout\WpOrm\Tests\WordPress\TestCase; 15 | use Illuminate\Database\Eloquent\Prunable; 16 | use Illuminate\Database\Schema\Blueprint; 17 | 18 | class PrunableTest extends TestCase 19 | { 20 | public static function setUpBeforeClass(): void 21 | { 22 | Database::getInstance()->getSchemaBuilder()->create('sales_payment', function (Blueprint $table) { 23 | $table->id(); 24 | $table->date('created_at'); 25 | $table->string('method'); 26 | $table->float('amount'); 27 | }); 28 | } 29 | 30 | /** 31 | * @return void 32 | * @covers Prunable::prunable() 33 | */ 34 | public function testPrunable(): void 35 | { 36 | $model = new class () extends AbstractModel { 37 | use Prunable; 38 | 39 | protected $primaryKey = 'id'; 40 | public $timestamps = false; 41 | protected $table = 'sales_payment'; 42 | 43 | public function prunable() 44 | { 45 | return static::where('created_at', '<', Carbon::create(2025, 1, 1)); 46 | } 47 | }; 48 | 49 | $values = [ 50 | [ 51 | 'method' => 'adyen', 52 | 'amount' => 100, 53 | 'created_at' => Carbon::create(2024, 05, 10), 54 | ], 55 | [ 56 | 'method' => 'paypal', 57 | 'amount' => 50.52, 58 | 'created_at' => Carbon::create(2023, 10, 8), 59 | ], 60 | [ 61 | 'method' => 'apple_pay', 62 | 'amount' => 145.12, 63 | 'created_at' => Carbon::create(2024, 5, 28), 64 | ], 65 | [ 66 | 'method' => 'amazon_pay', 67 | 'amount' => 199.99, 68 | 'created_at' => Carbon::create(2025, 4, 15), 69 | ], 70 | [ 71 | 'method' => 'swiss_pay', 72 | 'amount' => 129.50, 73 | 'created_at' => Carbon::create(2025, 3, 5), 74 | ], 75 | [ 76 | 'method' => 'adyen', 77 | 'amount' => 15.99, 78 | 'created_at' => Carbon::create(2024, 7, 19), 79 | ], 80 | [ 81 | 'method' => 'ideal', 82 | 'amount' => 69.10, 83 | 'created_at' => Carbon::create(2026, 4, 5), 84 | ], 85 | ]; 86 | 87 | $this->assertTrue($model::insert($values)); 88 | $deletedCount = (new $model())->pruneAll(); 89 | $this->assertEquals(4, $deletedCount); 90 | 91 | $result = $model::query()->whereDate('created_at', '<', Carbon::create(2025, 1, 1))->count(); 92 | $this->assertEquals(0, $result, 'It should no longer have value since all the rows were deleted before.'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/WordPress/Models/CommentTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Models; 10 | 11 | use Dbout\WpOrm\Models\Comment; 12 | use Dbout\WpOrm\Models\Post; 13 | use Dbout\WpOrm\Models\User; 14 | use Dbout\WpOrm\Tests\WordPress\TestCase; 15 | 16 | class CommentTest extends TestCase 17 | { 18 | /** 19 | * @return void 20 | * @covers Comment::user 21 | */ 22 | public function testUser(): void 23 | { 24 | $userId = self::factory()->user->create([ 25 | 'user_login' => 'test-15', 26 | 'user_pass' => 'testing', 27 | 'user_email' => 'test-15@test.com', 28 | ]); 29 | 30 | $comment = new Comment(); 31 | $comment->setUserId($userId); 32 | $comment->setCommentContent('Hello world'); 33 | $this->assertTrue($comment->save()); 34 | $this->assertEquals('Hello world', $comment->getCommentContent()); 35 | $this->assertEquals($userId, $comment->getUserId()); 36 | 37 | $reloadComment = Comment::find($comment->getId()); 38 | $user = $reloadComment->user; 39 | $this->assertLastQueryHasOneRelation('users', 'ID', $userId); 40 | 41 | $this->assertInstanceOf(User::class, $user); 42 | $this->assertEquals($userId, $user->getId()); 43 | } 44 | 45 | /** 46 | * @return void 47 | * @covers Comment::post 48 | */ 49 | public function testPost(): void 50 | { 51 | $postId = self::factory()->post->create([ 52 | 'post_type' => 'product', 53 | 'post_content' => 'product information', 54 | 'post_name' => 'product-15-10-15', 55 | ]); 56 | 57 | $comment = new Comment(); 58 | $comment->setCommentPostID($postId); 59 | $comment->setCommentType('woocommerce'); 60 | $comment->setCommentContent('custom notification'); 61 | 62 | $this->assertTrue($comment->save()); 63 | $this->assertEquals($postId, $comment->getCommentPostID()); 64 | 65 | $reloadComment = Comment::find($comment->getId()); 66 | $post = $reloadComment->post; 67 | $this->assertLastQueryHasOneRelation('posts', 'ID', $postId); 68 | 69 | $this->assertInstanceOf(Post::class, $post); 70 | $this->assertEquals($postId, $post->getId()); 71 | } 72 | 73 | /** 74 | * @return void 75 | * @covers Comment::parent 76 | */ 77 | public function testParent(): void 78 | { 79 | $objectId = self::factory()->comment->create([ 80 | 'comment_author_email' => 'test@test.fr', 81 | 'comment_content' => 'Comment content', 82 | ]); 83 | 84 | $comment = new Comment(); 85 | $comment->setCommentParent($objectId); 86 | $comment->setCommentType('woocommerce'); 87 | $comment->setCommentContent('custom notification'); 88 | 89 | $this->assertTrue($comment->save()); 90 | $this->assertEquals($objectId, $comment->getCommentParent()); 91 | 92 | $reloadComment = Comment::find($comment->getId()); 93 | $parent = $reloadComment->parent; 94 | $this->assertLastQueryHasOneRelation('comments', 'comment_ID', $objectId); 95 | 96 | $this->assertInstanceOf(Comment::class, $parent); 97 | $this->assertEquals($objectId, $parent->getId()); 98 | } 99 | 100 | /** 101 | * @return void 102 | * @covers Comment::save 103 | */ 104 | public function testSave(): void 105 | { 106 | $comment = new Comment(); 107 | $comment->setCommentContent('I think the piano on Sunset Jesus is a masterpiece.'); 108 | $comment->setCommentAuthor('15'); 109 | $comment->setCommentAuthorIP('127.0.0.1'); 110 | $comment->setCommentAgent('chrome'); 111 | $comment->setCommentAuthorEmail('test@test.com'); 112 | $comment->setCommentPostID(165); 113 | $comment->setCommentType('custom'); 114 | $comment->setCommentApproved('yes'); 115 | $comment->setCommentAuthorUrl('https://my-site.com'); 116 | $comment->setCommentParent(6525); 117 | 118 | $this->assertTrue($comment->save()); 119 | 120 | $loadComment = Comment::find($comment->getId()); 121 | $this->assertInstanceOf(Comment::class, $loadComment); 122 | $this->assertEquals('I think the piano on Sunset Jesus is a masterpiece.', $comment->getCommentContent()); 123 | $this->assertEquals('15', $comment->getCommentAuthor()); 124 | $this->assertEquals('127.0.0.1', $comment->getCommentAuthorIP()); 125 | $this->assertEquals('chrome', $comment->getCommentAgent()); 126 | $this->assertEquals('test@test.com', $comment->getCommentAuthorEmail()); 127 | $this->assertEquals(165, $comment->getCommentPostID()); 128 | $this->assertEquals('custom', $comment->getCommentType()); 129 | $this->assertEquals('yes', $comment->getCommentApproved()); 130 | $this->assertEquals('https://my-site.com', $comment->getCommentAuthorUrl()); 131 | $this->assertEquals(6525, $comment->getCommentParent()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/WordPress/Models/CustomCommentTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Models; 10 | 11 | use Dbout\WpOrm\Models\CustomComment; 12 | use Dbout\WpOrm\Tests\WordPress\TestCase; 13 | 14 | class CustomCommentTest extends TestCase 15 | { 16 | /** 17 | * @return void 18 | * @covers CustomComment::find 19 | */ 20 | public function testFindValidType(): void 21 | { 22 | $objectId = self::factory()->comment->create([ 23 | 'comment_type' => 'woocommerce', 24 | ]); 25 | 26 | $model = new class () extends CustomComment { 27 | protected string $_type = 'woocommerce'; 28 | }; 29 | 30 | $object = $model::find($objectId); 31 | $this->assertInstanceOf($model::class, $object); 32 | $this->assertEquals('woocommerce', $object->getCommentType()); 33 | $this->assertEquals($objectId, $object->getId()); 34 | $this->assertLastQueryEquals(sprintf( 35 | "select `#TABLE_PREFIX#comments`.* from `#TABLE_PREFIX#comments` where `#TABLE_PREFIX#comments`.`comment_ID` = %s and `comment_type` = 'woocommerce' limit 1", 36 | $objectId 37 | )); 38 | } 39 | 40 | /** 41 | * @return void 42 | * @covers CustomComment::find 43 | */ 44 | public function testFindWithDifferentType(): void 45 | { 46 | $objectId = self::factory()->comment->create([ 47 | 'comment_type' => 'woocommerce', 48 | ]); 49 | 50 | $model = new class () extends CustomComment { 51 | protected string $_type = 'author'; 52 | }; 53 | 54 | $object = $model::find($objectId); 55 | $this->assertNull($object); 56 | 57 | $this->assertLastQueryEquals(sprintf( 58 | "select `#TABLE_PREFIX#comments`.* from `#TABLE_PREFIX#comments` where `#TABLE_PREFIX#comments`.`comment_ID` = %s and `comment_type` = 'author' limit 1", 59 | $objectId 60 | )); 61 | } 62 | 63 | /** 64 | * @return void 65 | * @covers CustomComment::save 66 | */ 67 | public function testSave(): void 68 | { 69 | $model = new class () extends CustomComment { 70 | protected string $_type = 'author'; 71 | }; 72 | 73 | $comment = new $model(); 74 | $comment->setCommentAuthor('Norman FOSTER'); 75 | $comment->setCommentAuthorEmail('test@test.com'); 76 | $comment->setCommentContent('Hello world'); 77 | 78 | $this->assertTrue($comment->save()); 79 | $this->assertEquals('Hello world', $comment->getCommentContent()); 80 | $this->assertEquals('Norman FOSTER', $comment->getCommentAuthor()); 81 | $this->assertEquals('test@test.com', $comment->getCommentAuthorEmail()); 82 | $this->assertEquals('author', $comment->getCommentType()); 83 | 84 | $this->assertCommentEqualsToWpComment($comment); 85 | } 86 | 87 | /** 88 | * @return void 89 | * @covers CustomComment::all 90 | */ 91 | public function testAll(): void 92 | { 93 | $applicationCommentsV1 = self::factory()->comment->create_many(5, [ 94 | 'comment_type' => 'application', 95 | ]); 96 | 97 | self::factory()->comment->create_many(10, [ 98 | 'comment_type' => 'fake-type', 99 | ]); 100 | 101 | $applicationCommentsV2 = self::factory()->comment->create_many(7, [ 102 | 'comment_type' => 'application', 103 | ]); 104 | 105 | $model = new class () extends CustomComment { 106 | protected string $_type = 'application'; 107 | }; 108 | 109 | $comments = $model::all(); 110 | 111 | $this->assertLastQueryEquals( 112 | "select `#TABLE_PREFIX#comments`.* from `#TABLE_PREFIX#comments` where `comment_type` = 'application'" 113 | ); 114 | 115 | $applicationComments = array_merge($applicationCommentsV1, $applicationCommentsV2); 116 | $this->assertEquals(12, $comments->count()); 117 | $this->assertEquals($applicationComments, $comments->pluck('comment_ID')->toArray()); 118 | } 119 | 120 | /** 121 | * @return void 122 | * @covers CustomComment::update 123 | */ 124 | public function testUpdate(): void 125 | { 126 | $model = new class () extends CustomComment { 127 | protected string $_type = 'seo'; 128 | }; 129 | 130 | $comment = new $model([ 131 | 'comment_author' => 'Zaha HADID', 132 | 'comment_author_email' => 'test@test.com', 133 | 'comment_content' => 'My name is Zaha', 134 | ]); 135 | 136 | $this->assertTrue($comment->save()); 137 | $comment->setCommentAuthor('Jean Nouvel'); 138 | $comment->setCommentAuthorEmail('contact@jean-nouvel.fr'); 139 | $this->assertEquals('seo', $comment->getCommentType()); 140 | 141 | $this->assertTrue($comment->save()); 142 | $this->assertEquals('My name is Zaha', $comment->getCommentContent()); 143 | $this->assertEquals('Jean Nouvel', $comment->getCommentAuthor()); 144 | $this->assertEquals('contact@jean-nouvel.fr', $comment->getCommentAuthorEmail()); 145 | 146 | $this->assertCommentEqualsToWpComment($comment); 147 | } 148 | 149 | /** 150 | * @return void 151 | * @covers CustomComment::delete 152 | */ 153 | public function testDelete(): void 154 | { 155 | $model = new class () extends CustomComment { 156 | protected string $_type = 'seo'; 157 | }; 158 | 159 | $comment = new $model([ 160 | 'comment_author' => 'Zaha HADID', 161 | 'comment_author_email' => 'test@test.com', 162 | 'comment_content' => 'My name is Zaha', 163 | ]); 164 | 165 | $comment->save(); 166 | $commentId = $comment->getId(); 167 | $this->assertTrue($comment->delete()); 168 | $this->assertLastQueryEquals(sprintf( 169 | "delete from `#TABLE_PREFIX#comments` where `comment_ID` = %s", 170 | $commentId 171 | )); 172 | 173 | $wpComment = get_comment($commentId); 174 | $this->assertNull($wpComment); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/WordPress/Models/CustomPostTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Models; 10 | 11 | use Dbout\WpOrm\Models\CustomPost; 12 | use Dbout\WpOrm\Tests\WordPress\TestCase; 13 | 14 | class CustomPostTest extends TestCase 15 | { 16 | /** 17 | * @return void 18 | * @covers CustomPost::find 19 | */ 20 | public function testFindWithValidPostType(): void 21 | { 22 | $objectId = self::factory()->post->create([ 23 | 'post_type' => 'architect', 24 | ]); 25 | 26 | $model = new class () extends CustomPost { 27 | protected string $_type = 'architect'; 28 | }; 29 | 30 | $object = $model::find($objectId); 31 | $this->assertInstanceOf($model::class, $object); 32 | $this->assertEquals('architect', $object->getPostType()); 33 | $this->assertEquals($objectId, $object->getId()); 34 | 35 | $this->assertLastQueryEquals(sprintf( 36 | "select `#TABLE_PREFIX#posts`.* from `#TABLE_PREFIX#posts` where `#TABLE_PREFIX#posts`.`ID` = %s and `post_type` = 'architect' limit 1", 37 | $objectId 38 | )); 39 | } 40 | 41 | /** 42 | * @return void 43 | * @covers CustomPost::find 44 | */ 45 | public function testFindWithDifferentType(): void 46 | { 47 | $objectId = self::factory()->post->create([ 48 | 'post_type' => 'product', 49 | ]); 50 | 51 | $model = new class () extends CustomPost { 52 | protected string $_type = 'architect'; 53 | }; 54 | 55 | $object = $model::find($objectId); 56 | $this->assertNull($object, 'Value must be null because cannot load another post_type object.'); 57 | $this->assertLastQueryEquals(sprintf( 58 | "select `#TABLE_PREFIX#posts`.* from `#TABLE_PREFIX#posts` where `#TABLE_PREFIX#posts`.`ID` = %s and `post_type` = 'architect' limit 1", 59 | $objectId 60 | )); 61 | } 62 | 63 | /** 64 | * @return void 65 | * @covers CustomPost::save 66 | */ 67 | public function testSave(): void 68 | { 69 | $model = new class () extends CustomPost { 70 | protected string $_type = 'architect'; 71 | }; 72 | 73 | $architect = new $model(); 74 | $architect->setPostTitle('Norman FOSTER'); 75 | $architect->setPostContent('Content - My name is Norman FOSTER'); 76 | $architect->setPostExcerpt('Excerpt - My name is Norman FOSTER'); 77 | $architect->setPostStatus('publish'); 78 | $architect->setPostName('norman-foster'); 79 | 80 | $this->assertTrue($architect->save()); 81 | $this->assertEquals('Norman FOSTER', $architect->getPostTitle()); 82 | $this->assertEquals('Content - My name is Norman FOSTER', $architect->getPostContent()); 83 | $this->assertEquals('Excerpt - My name is Norman FOSTER', $architect->getPostExcerpt()); 84 | $this->assertEquals('publish', $architect->getPostStatus()); 85 | $this->assertEquals('norman-foster', $architect->getPostName()); 86 | $this->assertEquals('architect', $architect->getPostType()); 87 | 88 | $this->assertPostEqualsToWpPost($architect); 89 | } 90 | 91 | /** 92 | * @return void 93 | * @covers CustomPost::all 94 | */ 95 | public function testAll(): void 96 | { 97 | $objectsV1 = self::factory()->post->create_many(5, [ 98 | 'post_type' => 'project', 99 | ]); 100 | 101 | self::factory()->post->create_many(10, [ 102 | 'post_type' => 'fake-project', 103 | ]); 104 | 105 | $objectsV2 = self::factory()->post->create_many(7, [ 106 | 'post_type' => 'project', 107 | ]); 108 | 109 | $model = new class () extends CustomPost { 110 | protected string $_type = 'project'; 111 | }; 112 | 113 | $projects = $model::all(); 114 | 115 | $this->assertLastQueryEquals( 116 | "select `#TABLE_PREFIX#posts`.* from `#TABLE_PREFIX#posts` where `post_type` = 'project'" 117 | ); 118 | 119 | $objectIds = array_merge($objectsV1, $objectsV2); 120 | $this->assertEquals(12, $projects->count()); 121 | $this->assertEquals($objectIds, $projects->pluck('ID')->toArray()); 122 | } 123 | 124 | /** 125 | * @return void 126 | * @covers CustomPost::delete 127 | */ 128 | public function testDelete(): void 129 | { 130 | $model = new class () extends CustomPost { 131 | protected string $_type = 'order'; 132 | }; 133 | 134 | $order = new $model(); 135 | $order->setPostName('order-15'); 136 | $order->setPostTitle('Order #15'); 137 | $order->save(); 138 | 139 | $objectId = $order->getId(); 140 | $this->assertTrue($order->delete()); 141 | $this->assertLastQueryEquals(sprintf( 142 | "delete from `#TABLE_PREFIX#posts` where `ID` = %s", 143 | $objectId 144 | )); 145 | 146 | $wpObject = get_post($objectId); 147 | $this->assertNull($wpObject); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/WordPress/Models/OptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Models; 10 | 11 | use Dbout\WpOrm\Models\Option; 12 | use Dbout\WpOrm\Tests\WordPress\TestCase; 13 | 14 | class OptionTest extends TestCase 15 | { 16 | /** 17 | * @return void 18 | * @covers Option::findOneByName 19 | * @covers Option::getOptionName 20 | * @covers Option::getOptionValue 21 | */ 22 | public function testFindOneByName(): void 23 | { 24 | add_option('my_custom_option', 'option_value'); 25 | $option = Option::findOneByName('my_custom_option'); 26 | 27 | $this->assertInstanceOf(Option::class, $option); 28 | $this->assertFindLastQuery('options', 'option_name', 'my_custom_option'); 29 | $this->assertEquals('option_value', $option->getOptionValue()); 30 | $this->assertEquals('my_custom_option', $option->getOptionName()); 31 | } 32 | 33 | /** 34 | * @return void 35 | * @covers Option::findOneByName 36 | */ 37 | public function testFindOneByNameWithNotFound(): void 38 | { 39 | add_option('my_custom_option', 'option_value'); 40 | $option = Option::findOneByName('my_custom_option_fake'); 41 | $this->assertNull($option); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/WordPress/Models/PostTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Models; 10 | 11 | use Dbout\WpOrm\Models\Post; 12 | use Dbout\WpOrm\Models\User; 13 | use Dbout\WpOrm\Tests\WordPress\TestCase; 14 | 15 | class PostTest extends TestCase 16 | { 17 | /** 18 | * @return void 19 | * @covers Post::findOneByGuid 20 | */ 21 | public function testFindOneByGuid(): void 22 | { 23 | self::factory()->post->create([ 24 | 'post_type' => 'product', 25 | 'post_content' => 'product information', 26 | 'post_name' => 'product-testFindOneByGuid', 27 | // Auto add http:// 28 | 'guid' => 'guid-testFindOneByGuid', 29 | ]); 30 | 31 | $post = Post::findOneByGuid('http://guid-testFindOneByGuid'); 32 | $this->assertInstanceOf(Post::class, $post); 33 | $this->assertPostEqualsToWpPost($post); 34 | } 35 | 36 | /** 37 | * @return void 38 | * @covers Post::findOneByGuid 39 | */ 40 | public function testFindOneByGuidWithNotFound(): void 41 | { 42 | self::factory()->post->create([ 43 | 'post_type' => 'product', 44 | 'post_content' => 'product information', 45 | 'post_name' => 'product-testFindOneByName', 46 | 'guid' => 'guid-testFindOneByName', 47 | ]); 48 | 49 | $post = Post::findOneByGuid('http://guid-testFindOneByName-fake'); 50 | $this->assertNull($post); 51 | } 52 | 53 | /** 54 | * @return void 55 | * @covers Post::findOneByName 56 | */ 57 | public function testFindOneByName(): void 58 | { 59 | self::factory()->post->create([ 60 | 'post_type' => 'product', 61 | 'post_content' => 'product AABB', 62 | 'post_name' => 'product-testFindOneByName', 63 | 'guid' => 'guid-testFindOneByName', 64 | ]); 65 | 66 | $post = Post::findOneByName('product-testFindOneByName'); 67 | $this->assertInstanceOf(Post::class, $post); 68 | $this->assertPostEqualsToWpPost($post); 69 | } 70 | 71 | /** 72 | * @return void 73 | * @covers Post::findOneByName 74 | */ 75 | public function testFindOneByNameWithNotFound(): void 76 | { 77 | self::factory()->post->create([ 78 | 'post_type' => 'product', 79 | 'post_content' => 'product AABB', 80 | 'post_name' => 'product-testFindOneByNameWithNotFound', 81 | 'guid' => 'guid-testFindOneByNameWithNotFound', 82 | ]); 83 | 84 | $post = Post::findOneByName('product-testFindOneByNameWithNotFound-fake'); 85 | $this->assertNull($post); 86 | } 87 | 88 | /** 89 | * @return void 90 | * @covers Post::parent 91 | */ 92 | public function testParent(): void 93 | { 94 | $objectId = self::factory()->post->create([ 95 | 'post_type' => 'product', 96 | 'post_content' => 'Child product XX', 97 | 'post_name' => 'child-product', 98 | ]); 99 | 100 | $object = new Post(); 101 | $object->setPostName('product-test'); 102 | $object->setPostTitle('Product test'); 103 | $object->setPostParent($objectId); 104 | 105 | $this->assertTrue($object->save()); 106 | $this->assertEquals($objectId, $object->getPostParent()); 107 | 108 | $newObject = Post::find($object->getId()); 109 | $parent = $newObject->parent; 110 | $this->assertLastQueryHasOneRelation('posts', 'ID', $objectId); 111 | 112 | $this->assertInstanceOf(Post::class, $parent); 113 | $this->assertEquals($objectId, $parent->getId()); 114 | } 115 | 116 | /** 117 | * @return void 118 | * @covers Post::author 119 | */ 120 | public function testAuthor(): void 121 | { 122 | $userId = self::factory()->user->create([ 123 | 'user_login' => 'test-post-15', 124 | 'user_pass' => 'testing', 125 | 'user_email' => 'test-post-15@test.com', 126 | ]); 127 | 128 | $object = new Post(); 129 | $object->setPostAuthor($userId); 130 | $object->setPostType('product'); 131 | $object->setPostContent('Custom post content'); 132 | $object->setPostName('custom-post-content'); 133 | $this->assertTrue($object->save()); 134 | 135 | $this->assertPostEqualsToWpPost($object); 136 | 137 | $newObject = Post::find($object->getId()); 138 | $author = $newObject->author; 139 | $this->assertLastQueryHasOneRelation('users', 'ID', $userId); 140 | $this->assertInstanceOf(User::class, $author); 141 | $this->assertEquals($userId, $author->getId()); 142 | } 143 | 144 | /** 145 | * @return void 146 | * @covers Post::comments 147 | */ 148 | public function testComments(): void 149 | { 150 | /** 151 | * Create fake post with any relation with post 152 | */ 153 | self::factory()->comment->create([ 154 | 'comment_post_ID' => 1585, 155 | ]); 156 | 157 | $post = new Post(); 158 | $post->setPostTitle('Norman FOSTER - British Museum'); 159 | $post->setPostName('norman-foster-british-museum'); 160 | $post->setPostContent('Lorem ipsum dolor sit amet'); 161 | $this->assertTrue($post->save()); 162 | 163 | $ids = self::factory()->comment->create_many(3, [ 164 | 'comment_post_ID' => $post->getId(), 165 | ]); 166 | 167 | $this->assertHasManyRelation( 168 | expectedItems: $post->comments, 169 | relationProperty: 'comment_ID', 170 | expectedIds: $ids 171 | ); 172 | } 173 | 174 | /** 175 | * @return void 176 | * @covers Post::save 177 | */ 178 | public function testSave(): void 179 | { 180 | $post = new Post(); 181 | $post->setPostTitle('Avicii - The best DJ in the world'); 182 | $post->setPostName('avicii-the-best-dj-in-the-world'); 183 | $post->setPostContent('Praesent turpis sapien, hendrerit.'); 184 | $post->setPostType('artist'); 185 | $post->setPostExcerpt('Fusce consequat tellus augue.'); 186 | $post->setPostStatus('closed'); 187 | $post->setPostPassword('avicii-pwd'); 188 | $post->setCommentStatus('opened'); 189 | $post->setPostAuthor(158); 190 | $post->setPostParent(86585); 191 | $post->setMenuOrder(15); 192 | 193 | $this->assertTrue($post->save()); 194 | $loadedPost = Post::find($post->getId()); 195 | 196 | $this->assertInstanceOf(Post::class, $loadedPost); 197 | $this->assertEquals('Avicii - The best DJ in the world', $post->getPostTitle()); 198 | $this->assertEquals('avicii-the-best-dj-in-the-world', $post->getPostName()); 199 | $this->assertEquals('Praesent turpis sapien, hendrerit.', $post->getPostContent()); 200 | $this->assertEquals('artist', $post->getPostType()); 201 | $this->assertEquals('Fusce consequat tellus augue.', $post->getPostExcerpt()); 202 | $this->assertEquals('closed', $post->getPostStatus()); 203 | $this->assertEquals('avicii-pwd', $post->getPostPassword()); 204 | $this->assertEquals('opened', $post->getCommentStatus()); 205 | $this->assertEquals(158, $post->getPostAuthor()); 206 | $this->assertEquals(86585, $post->getPostParent()); 207 | $this->assertEquals(15, $post->getMenuOrder()); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/WordPress/Models/UserTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Models; 10 | 11 | use Dbout\WpOrm\Models\User; 12 | use Dbout\WpOrm\Tests\WordPress\TestCase; 13 | 14 | class UserTest extends TestCase 15 | { 16 | private const USER_EMAIL = 'wp-testing@wp-orm.fr'; 17 | private const USER_LOGIN = 'testing.wp-orm'; 18 | private static ?int $testingUserId = null; 19 | 20 | /** 21 | * User created just to simulate a database with multiple users. 22 | * 23 | * @var int|null 24 | */ 25 | private static ?int $fakeUserId = null; 26 | 27 | /** 28 | * @return void 29 | */ 30 | public static function setUpBeforeClass(): void 31 | { 32 | self::$testingUserId = self::factory()->user->create([ 33 | 'user_login' => self::USER_LOGIN, 34 | 'user_pass' => 'testing', 35 | 'user_email' => self::USER_EMAIL, 36 | ]); 37 | 38 | self::$fakeUserId = self::factory()->user->create(); 39 | } 40 | 41 | /** 42 | * @return void 43 | * @covers User::findOneByEmail 44 | */ 45 | public function testFindOneByEmail(): void 46 | { 47 | $this->checkFindOneResult( 48 | User::findOneByEmail(self::USER_EMAIL), 49 | 'user_email', 50 | self::USER_EMAIL 51 | ); 52 | } 53 | 54 | /** 55 | * @return void 56 | * @covers User::findOneByLogin 57 | */ 58 | public function testFindOneByLogin(): void 59 | { 60 | $this->checkFindOneResult( 61 | User::findOneByLogin(self::USER_LOGIN), 62 | 'user_login', 63 | self::USER_LOGIN 64 | ); 65 | } 66 | 67 | /** 68 | * @return void 69 | * @covers User::comments 70 | */ 71 | public function testComments(): void 72 | { 73 | /** 74 | * Create fake comment with any relation with user 75 | */ 76 | self::factory()->comment->create([ 77 | 'user_id' => self::$fakeUserId, 78 | ]); 79 | 80 | $ids = self::factory()->comment->create_many(2, [ 81 | 'user_id' => self::$testingUserId, 82 | ]); 83 | 84 | $this->assertHasManyRelation( 85 | expectedItems: $this->getTestingUser()?->comments, 86 | relationProperty: 'comment_ID', 87 | expectedIds: $ids 88 | ); 89 | } 90 | 91 | /** 92 | * @return void 93 | * @covers User::posts 94 | */ 95 | public function testPosts(): void 96 | { 97 | /** 98 | * Create fake post with any relation with user 99 | */ 100 | self::factory()->post->create([ 101 | 'user_id' => self::$fakeUserId, 102 | ]); 103 | 104 | $ids = self::factory()->post->create_many(3, [ 105 | 'post_author' => self::$testingUserId, 106 | ]); 107 | 108 | $this->assertHasManyRelation( 109 | expectedItems: $this->getTestingUser()?->posts, 110 | relationProperty: 'ID', 111 | expectedIds: $ids 112 | ); 113 | } 114 | 115 | /** 116 | * @param User|null $user 117 | * @param string $whereColumn 118 | * @param string $whereValue 119 | * @return void 120 | */ 121 | private function checkFindOneResult(?User $user, string $whereColumn, string $whereValue): void 122 | { 123 | $this->assertInstanceOf(User::class, $user); 124 | $this->assertFindLastQuery('users', $whereColumn, $whereValue); 125 | 126 | $this->assertEquals(self::$testingUserId, $user->getId()); 127 | $this->assertEquals(self::USER_LOGIN, $user->getUserLogin()); 128 | $this->assertEquals(self::USER_EMAIL, $user->getUserEmail()); 129 | } 130 | 131 | /** 132 | * @return User|null 133 | */ 134 | private function getTestingUser(): ?User 135 | { 136 | return User::find(self::$testingUserId); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/WordPress/Orm/AbstractModelTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Orm; 10 | 11 | use Dbout\WpOrm\Models\Article; 12 | use Dbout\WpOrm\Models\Option; 13 | use Dbout\WpOrm\Models\Post; 14 | use Dbout\WpOrm\Tests\WordPress\TestCase; 15 | use Illuminate\Database\QueryException; 16 | 17 | /** 18 | * @coversDefaultClass \Dbout\WpOrm\Orm\AbstractModel 19 | */ 20 | class AbstractModelTest extends TestCase 21 | { 22 | /** 23 | * @param string $saveMethod 24 | * @return void 25 | * @covers ::save 26 | * @covers ::saveOrFail 27 | * @dataProvider providerTestSaveNewObject 28 | */ 29 | public function testSuccessSaveNewObject(string $saveMethod): void 30 | { 31 | $model = new Article(); 32 | $model->setPostName('hello-world'); 33 | $model->setPostContent('My hello world content'); 34 | $model->setPostTitle('Hello world'); 35 | 36 | $this->assertTrue($model->$saveMethod()); 37 | $expectedId = $model->getId(); 38 | $this->assertIsNumeric($expectedId); 39 | 40 | $this->assertPostEqualsToWpPost($model); 41 | $this->assertEqualLastInsertId($expectedId); 42 | } 43 | 44 | /** 45 | * @param string $saveMethod 46 | * @return void 47 | * @covers ::save 48 | * @covers ::saveOrFail 49 | * @dataProvider providerTestSaveNewObject 50 | */ 51 | public function testSaveWithInvalidProperty(string $saveMethod): void 52 | { 53 | $fakeColumn = 'custom_column'; 54 | $model = new Article([ 55 | $fakeColumn => '15', 56 | ]); 57 | 58 | $this->expectException(QueryException::class); 59 | $this->expectExceptionUnknownColumn($fakeColumn); 60 | $model->$saveMethod(); 61 | } 62 | 63 | /** 64 | * @return \Generator 65 | */ 66 | protected function providerTestSaveNewObject(): \Generator 67 | { 68 | yield 'With save function' => [ 69 | 'save', 70 | ]; 71 | 72 | yield 'With saveOrFail function' => [ 73 | 'saveOrFail', 74 | ]; 75 | } 76 | 77 | /** 78 | * @return void 79 | * @covers ::delete 80 | */ 81 | public function testDelete(): void 82 | { 83 | $postId = self::factory()->post->create(); 84 | $post = Post::find($postId); 85 | 86 | $this->assertInstanceOf(Post::class, $post); 87 | $this->assertTrue($post->delete()); 88 | 89 | $post = Post::find($postId); 90 | $this->assertNull($post, 'The post was not deleted correctly.'); 91 | } 92 | 93 | /** 94 | * Test if all attributes have been overridden. 95 | * 96 | * @return void 97 | * @covers ::fill 98 | * @covers ::guard 99 | */ 100 | public function testFillWithEmptyGuarded(): void 101 | { 102 | $post = new Post(); 103 | $post->setPostType('article'); 104 | $post->setPostName('the-article'); 105 | $post->setPostContent('the article content'); 106 | $post->guard([]); 107 | $post->fill([ 108 | 'post_type' => 'product', 109 | 'post_name' => 'my-filled-post', 110 | 'post_content' => 'The post content', 111 | ]); 112 | 113 | $this->assertEquals('product', $post->getPostType()); 114 | $this->assertEquals('my-filled-post', $post->getPostName()); 115 | $this->assertEquals('The post content', $post->getPostContent()); 116 | } 117 | 118 | /** 119 | * @return void 120 | * @covers ::fill 121 | * @covers ::guard 122 | */ 123 | public function testFillWithGuardedAttributes(): void 124 | { 125 | $post = new Post(); 126 | $post->setPostType('article'); 127 | $post->setPostName('the-article'); 128 | $post->setPostContent('the article content.'); 129 | $post->guard(['post_type']); 130 | $post->fill([ 131 | 'post_type' => 'product', 132 | 'post_name' => 'my-filled-post', 133 | 'post_content' => 'The post content', 134 | 'test' => 'custom test column', 135 | ]); 136 | 137 | $this->assertEquals('article', $post->getPostType(), 'This attribute should not be changed because it is protected.'); 138 | $this->assertEquals('my-filled-post', $post->getPostName()); 139 | $this->assertEquals('The post content', $post->getPostContent()); 140 | $this->assertNull($post->getAttribute('test'), 'This attribute must be empty because it does not exist in the posts table.'); 141 | } 142 | 143 | /** 144 | * @return void 145 | * @covers ::upsert 146 | */ 147 | public function testUpsertWithOneNewObjects(): void 148 | { 149 | Option::upsert( 150 | [ 151 | [ 152 | 'option_name' => '_upsert_architect_0', 153 | 'option_value' => 'John D.', 154 | ], 155 | [ 156 | 'option_name' => '_upsert_architect_1', 157 | 'option_value' => 'Zaha H.', 158 | ], 159 | ], 160 | ['option_name'] 161 | ); 162 | 163 | $this->checkUpsertOption('_upsert_architect_0', 'John D.'); 164 | $this->checkUpsertOption('_upsert_architect_1', 'Zaha H.'); 165 | } 166 | 167 | /** 168 | * @return void 169 | * @covers ::upsert 170 | */ 171 | public function testUpsertWithExistingObjects(): void 172 | { 173 | add_option('store_phone', '15 15 15'); 174 | add_option('store_email', 'boutique@test.fr'); 175 | add_option('store_address', 'Road test'); 176 | 177 | Option::upsert( 178 | [ 179 | [ 180 | 'option_name' => 'store_phone', 181 | 'option_value' => '15 15 15', 182 | ], 183 | [ 184 | 'option_name' => 'store_email', 185 | 'option_value' => 'boutique@test.fr', 186 | ], 187 | [ 188 | 'option_name' => 'store_address', 189 | 'option_value' => 'Road of paris', 190 | ], 191 | ], 192 | ['option_name'] 193 | ); 194 | 195 | $this->checkUpsertOption('store_phone', '15 15 15'); 196 | $this->checkUpsertOption('store_email', 'boutique@test.fr'); 197 | 198 | // Check if value is updated 199 | $this->checkUpsertOption('store_address', 'Road of paris'); 200 | } 201 | 202 | /** 203 | * @return void 204 | * @covers ::upsert 205 | */ 206 | public function testUpsertWithUpdateKey(): void 207 | { 208 | add_option('store_latitude', '75.652'); 209 | 210 | Option::upsert( 211 | [ 212 | [ 213 | 'option_name' => 'store_latitude', 214 | 'option_value' => 40.111, 215 | 'autoload' => 'no', 216 | ], 217 | ], 218 | ['option_name'], 219 | ['autoload'] 220 | ); 221 | 222 | // Check if value is not update updated 223 | $option = $this->checkUpsertOption('store_latitude', 75.652); 224 | $this->assertEquals('no', $option?->getAutoload()); 225 | } 226 | 227 | /** 228 | * @param string $optionName 229 | * @param mixed $expectedValue 230 | * @return Option|null 231 | */ 232 | private function checkUpsertOption(string $optionName, mixed $expectedValue): ?Option 233 | { 234 | $option = Option::findOneByName($optionName); 235 | $this->assertInstanceOf(Option::class, $option); 236 | $this->assertEquals($expectedValue, $option->getOptionValue()); 237 | 238 | \wp_cache_delete('alloptions', 'options'); 239 | $wpOpt = \get_option($optionName); 240 | $this->assertEquals($expectedValue, $wpOpt); 241 | 242 | return $option; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/WordPress/Orm/AbstractModelWithCustomTableTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Orm; 10 | 11 | use Dbout\WpOrm\Orm\AbstractModel; 12 | use Dbout\WpOrm\Tests\WordPress\TestCase; 13 | 14 | class AbstractModelWithCustomTableTest extends TestCase 15 | { 16 | private const TABLE_NAME = 'custom_table'; 17 | private static AbstractModel $model; 18 | 19 | /** 20 | * @return void 21 | */ 22 | public static function setUpBeforeClass(): void 23 | { 24 | global $wpdb; 25 | 26 | $tableName = $wpdb->prefix . self::TABLE_NAME; 27 | $sql = "CREATE TABLE $tableName ( 28 | id INT NOT NULL AUTO_INCREMENT, 29 | name varchar(100) NOT NULL, 30 | url varchar(55) DEFAULT '' NOT NULL, 31 | metadata JSON, 32 | PRIMARY KEY (id) 33 | );"; 34 | 35 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 36 | dbDelta($sql); 37 | 38 | self::$model = new class () extends AbstractModel { 39 | protected $primaryKey = 'id'; 40 | public $timestamps = false; 41 | 42 | protected $table = 'custom_table'; 43 | 44 | protected $casts = [ 45 | 'metadata' => 'json', 46 | ]; 47 | }; 48 | } 49 | 50 | /** 51 | * @return void 52 | */ 53 | public function setUp(): void 54 | { 55 | self::$model::truncate(); 56 | } 57 | 58 | /** 59 | * @return void 60 | * @covers AbstractModel::save 61 | */ 62 | public function testSave(): void 63 | { 64 | $metaData = ['birthday-date' => '01-06-1935', 'address' => ['city' => 'London', 'zipcode' => 'London']]; 65 | 66 | /** @var AbstractModel $object */ 67 | $object = new self::$model(); 68 | $object->setAttribute('name', 'Norman FOSTER'); 69 | $object->setAttribute('url', 'norman-forster'); 70 | $object->setAttribute('metadata', $metaData); 71 | $this->assertTrue($object->save()); 72 | 73 | $this->assertEquals('Norman FOSTER', $object->getAttribute('name')); 74 | $this->assertEquals('norman-forster', $object->getAttribute('url')); 75 | $this->assertEquals($metaData, $object->getAttribute('metadata')); 76 | } 77 | 78 | /** 79 | * @return void 80 | * @covers AbstractModel::find 81 | */ 82 | public function testFind(): void 83 | { 84 | /** @var AbstractModel $object */ 85 | $object = new self::$model([ 86 | 'name' => 'Zaha Hadid', 87 | 'url' => 'zaha-hadid', 88 | 'metadata' => ['birthday-date' => '31-10-1950', 'address' => ['city' => 'Bagdad']], 89 | ]); 90 | 91 | $this->assertTrue($object->save()); 92 | 93 | $newObject = self::$model::find($object->getId()); 94 | $this->assertInstanceOf(AbstractModel::class, $newObject); 95 | } 96 | 97 | /** 98 | * @return void 99 | * @covers AbstractModel::query 100 | */ 101 | public function testWhereWithComplexJsonColumn(): void 102 | { 103 | $seArtists = [ 104 | [ 105 | 'name' => 'Avicii', 106 | 'url' => 'avicii', 107 | 'metadata' => ['birthday-date' => '08-09-1989', 'address' => ['city' => 'Stockholm', 'country' => 'SE']], 108 | ], 109 | [ 110 | 'name' => 'Basshunter', 111 | 'url' => 'basshunter', 112 | 'metadata' => ['birthday-date' => '22-12-1984', 'address' => ['city' => 'Halmstad', 'country' => 'SE']], 113 | ], 114 | [ 115 | 'name' => 'Måns Zelmerlöw', 116 | 'url' => 'mans-zelmerlow', 117 | 'metadata' => ['birthday-date' => '11-06-1986', 'address' => ['city' => 'Lund', 'country' => 'SE']], 118 | ], 119 | ]; 120 | 121 | $seIds = []; 122 | foreach ($seArtists as $artist) { 123 | $model = new self::$model($artist); 124 | $model->save(); 125 | $seIds[] = $model->getId(); 126 | } 127 | 128 | $frArtists = [ 129 | [ 130 | 'name' => 'Madeon', 131 | 'url' => 'madeon', 132 | 'metadata' => ['birthday-date' => '30-05-1994', 'address' => ['city' => 'Nantes', 'country' => 'FR']], 133 | ], 134 | [ 135 | 'name' => 'Kavinsky', 136 | 'url' => 'kavinsky', 137 | 'metadata' => ['birthday-date' => '31-07-1975', 'address' => ['city' => 'Seine-Saint-Denis', 'country' => 'FR']], 138 | ], 139 | ]; 140 | 141 | foreach ($frArtists as $artist) { 142 | $model = new self::$model($artist); 143 | $model->save(); 144 | } 145 | 146 | $selectedIds = self::$model::query()->where('metadata->address.country', 'SE')->get()->pluck('id')->toArray(); 147 | $this->assertLastQueryEquals("select * from `#TABLE_PREFIX#custom_table` where json_unquote(json_extract(`metadata`, '$.address.country')) = 'SE'"); 148 | $this->assertEquals($seIds, $selectedIds); 149 | } 150 | 151 | /** 152 | * @return void 153 | * @covers AbstractModel::query 154 | */ 155 | public function testWhereWithSimpleJsonColumn(): void 156 | { 157 | $edmArtists = [ 158 | [ 159 | 'name' => 'Martin Garrix', 160 | 'url' => 'martin-garrix', 161 | 'metadata' => ['type' => 'edm'], 162 | ], 163 | [ 164 | 'name' => 'Marshmello', 165 | 'url' => 'marshmello', 166 | 'metadata' => ['type' => 'edm'], 167 | ], 168 | [ 169 | 'name' => 'Calvin Harris', 170 | 'url' => 'calvin-harris', 171 | 'metadata' => ['type' => 'edm'], 172 | ], 173 | ]; 174 | 175 | $edmIds = []; 176 | foreach ($edmArtists as $artist) { 177 | $model = new self::$model($artist); 178 | $model->save(); 179 | $edmIds[] = $model->getId(); 180 | } 181 | 182 | $popArtists = [ 183 | [ 184 | 'name' => 'Coldplay', 185 | 'url' => 'coldplay', 186 | 'metadata' => ['type' => 'pop'], 187 | ], 188 | ]; 189 | 190 | foreach ($popArtists as $artist) { 191 | $model = new self::$model($artist); 192 | $model->save(); 193 | } 194 | 195 | $selectedIds = self::$model::query()->where('metadata->type', 'edm')->get()->pluck('id')->toArray(); 196 | $this->assertLastQueryEquals("select * from `#TABLE_PREFIX#custom_table` where json_unquote(json_extract(`metadata`, '$.type')) = 'edm'"); 197 | $this->assertEquals($edmIds, $selectedIds); 198 | } 199 | 200 | /** 201 | * @return void 202 | * @covers AbstractModel::delete 203 | */ 204 | public function testDelete(): void 205 | { 206 | /** @var AbstractModel $object */ 207 | $object = new self::$model([ 208 | 'name' => 'Frank Gehry', 209 | 'url' => 'frank-gehry', 210 | 'metadata' => ['birthday-date' => '28-02-1929', 'address' => ['city' => 'Toronto']], 211 | ]); 212 | 213 | $this->assertTrue($object->save()); 214 | $id = $object->getId(); 215 | $this->assertTrue($object->delete()); 216 | 217 | $newObject = self::$model::find($id); 218 | $this->assertNull($newObject); 219 | } 220 | 221 | /** 222 | * @return void 223 | * @covers AbstractModel::truncate 224 | */ 225 | public function testTruncate(): void 226 | { 227 | $data = [ 228 | [ 229 | 'name' => 'Martin Garrix', 230 | 'url' => 'martin-garrix', 231 | 'metadata' => ['type' => 'edm'], 232 | ], 233 | [ 234 | 'name' => 'Marshmello', 235 | 'url' => 'marshmello', 236 | 'metadata' => ['type' => 'edm'], 237 | ], 238 | [ 239 | 'name' => 'Calvin Harris', 240 | 'url' => 'calvin-harris', 241 | 'metadata' => ['type' => 'edm'], 242 | ], 243 | ]; 244 | 245 | foreach ($data as $item) { 246 | /** @var AbstractModel $e */ 247 | $e = new self::$model($item); 248 | $e->save(); 249 | } 250 | 251 | $this->assertEquals(3, self::$model::all()->count()); 252 | 253 | self::$model::truncate(); 254 | $this->assertEquals(0, self::$model::all()->count()); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /tests/WordPress/Orm/DatabaseTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Orm; 10 | 11 | use Dbout\WpOrm\Models\Post; 12 | use Dbout\WpOrm\Orm\Database; 13 | use Dbout\WpOrm\Tests\WordPress\TestCase; 14 | 15 | class DatabaseTest extends TestCase 16 | { 17 | private Database $database; 18 | 19 | /** 20 | * @return void 21 | */ 22 | public function setUp(): void 23 | { 24 | $this->database = Database::getInstance(); 25 | } 26 | 27 | /** 28 | * @return void 29 | * @covers Database::getTablePrefix 30 | */ 31 | public function testGetTablePrefix(): void 32 | { 33 | global $wpdb; 34 | $this->assertEquals($wpdb->prefix, $this->database->getTablePrefix()); 35 | } 36 | 37 | /** 38 | * @return void 39 | * @covers Database::getDatabaseName 40 | * @covers Database::getConfig 41 | */ 42 | public function testGetDatabaseName(): void 43 | { 44 | $this->assertEquals('wordpress_test', $this->database->getDatabaseName()); 45 | } 46 | 47 | /** 48 | * @return void 49 | * @covers Database::getName 50 | * @covers Database::getConfig 51 | */ 52 | public function testGetName(): void 53 | { 54 | $this->assertEquals('wp-eloquent-mysql2', $this->database->getName()); 55 | } 56 | 57 | /** 58 | * @param string $table 59 | * @param string|null $alias 60 | * @param string $expectedQuery 61 | * @return void 62 | * @covers Database::table 63 | * @dataProvider providerTestTable 64 | */ 65 | public function testTable(string $table, ?string $alias, string $expectedQuery): void 66 | { 67 | $builder = $this->database->table($table, $alias); 68 | $this->assertEquals($expectedQuery, $builder->toSql()); 69 | } 70 | 71 | /** 72 | * @return \Generator 73 | */ 74 | protected function providerTestTable(): \Generator 75 | { 76 | yield 'Without alias' => [ 77 | 'options', 78 | null, 79 | sprintf('select * from `%s`', $this->getTable('options')), 80 | ]; 81 | 82 | yield 'With alias' => [ 83 | 'options', 84 | 'opts', 85 | sprintf('select * from `%s` as `opts`', $this->getTable('options')), 86 | ]; 87 | } 88 | 89 | /** 90 | * @return void 91 | * @covers Database::lastInsertId 92 | */ 93 | public function testLastInsertId(): void 94 | { 95 | $post = new Post(); 96 | $post->setPostType('product'); 97 | 98 | $post->save(); 99 | $this->assertEquals(Database::getInstance()->lastInsertId(), $post->getId()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/WordPress/Orm/DatabaseTransactionTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Orm; 10 | 11 | use Dbout\WpOrm\Orm\AbstractModel; 12 | use Dbout\WpOrm\Orm\Database; 13 | use Dbout\WpOrm\Tests\WordPress\TestCase; 14 | use Illuminate\Database\QueryException; 15 | 16 | class DatabaseTransactionTest extends TestCase 17 | { 18 | private string $tableName = ''; 19 | private AbstractModel $model; 20 | private Database $db; 21 | 22 | /** 23 | * @return void 24 | */ 25 | public static function setUpBeforeClass(): void 26 | { 27 | global $wpdb; 28 | 29 | $tableName = $wpdb->prefix . 'document'; 30 | $sql = "CREATE TABLE $tableName ( 31 | id INT NOT NULL AUTO_INCREMENT, 32 | name varchar(100) NOT NULL, 33 | url varchar(55) DEFAULT '' NOT NULL, 34 | PRIMARY KEY (id) 35 | );"; 36 | 37 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 38 | dbDelta($sql); 39 | define('SAVEQUERIES', true); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | public function setUp(): void 46 | { 47 | $this->model = new class () extends AbstractModel { 48 | protected $primaryKey = 'id'; 49 | public $timestamps = false; 50 | protected $table = 'document'; 51 | }; 52 | 53 | global $wpdb; 54 | $this->tableName = $wpdb->prefix . 'document'; 55 | $this->model::truncate(); 56 | $this->db = Database::getInstance(); 57 | } 58 | 59 | /** 60 | * @throws \Throwable 61 | * @return void 62 | * @covers Database::transaction 63 | * @covers Database::insert 64 | * @covers Database::commit 65 | */ 66 | public function testTransactionCommit(): void 67 | { 68 | $this->resetLogQueries(); 69 | $this->db->transaction(function () { 70 | $query = sprintf('INSERT INTO %s (name, url) VALUES(?, ?);', $this->tableName); 71 | $this->db->insert($query, ['Invoice #15', 'invoice-15']); 72 | $this->db->insert($query, ['Invoice #16', 'invoice-16']); 73 | }); 74 | 75 | $this->assertTransaction('commit'); 76 | $this->assertCount(2, $this->model::all()->toArray()); 77 | } 78 | 79 | /** 80 | * @throws \Throwable 81 | * @return void 82 | * @covers Database::transaction 83 | * @covers Database::delete 84 | * @covers Database::insert 85 | * @covers Database::rollBack 86 | */ 87 | public function testTransactionRollback(): void 88 | { 89 | $query = sprintf('INSERT INTO %s (name, url) VALUES(?, ?);', $this->tableName); 90 | $this->db->insert($query, ['Deposit #1', 'deposit-1']); 91 | $this->db->insert($query, ['Deposit #2', 'deposit-2']); 92 | 93 | $this->resetLogQueries(); 94 | try { 95 | $this->db->transaction(function () use ($query) { 96 | $this->db->insert($query, ['Deposit #99', 'deposit-99']); 97 | $this->db->delete(sprintf('DELETE FROM %s;', $this->tableName)); 98 | 99 | /** 100 | * Throw exception because fake_column is invalid column name. 101 | */ 102 | $this->db->delete(sprintf('DELETE FROM %s WHERE fake_column = %d;', $this->tableName, $query)); 103 | }); 104 | } catch (\Exception) { 105 | // Off exception 106 | } 107 | 108 | $this->assertTransaction('rollback'); 109 | 110 | $items = $this->model::all(); 111 | $this->assertCount(2, $items->toArray(), 'There must be only 2 items because the transaction was rollback.'); 112 | $this->assertEquals(['deposit-1', 'deposit-2'], $items->pluck('url')->toArray()); 113 | } 114 | 115 | /** 116 | * @throws \Throwable 117 | * @return void 118 | * @covers Database::transaction 119 | */ 120 | public function testTransactionThrowsQueryException(): void 121 | { 122 | $this->expectException(QueryException::class); 123 | $this->resetLogQueries(); 124 | $this->db->transaction(function () { 125 | $this->db->delete('DELETE FROM fake_table;'); 126 | }); 127 | 128 | $this->assertTransaction('rollback'); 129 | } 130 | 131 | /** 132 | * @throws \Throwable 133 | * @return void 134 | * @covers Database::beginTransaction 135 | */ 136 | public function testBeginTransaction(): void 137 | { 138 | $this->db->beginTransaction(); 139 | $this->assertLastQueryEquals('START TRANSACTION;'); 140 | } 141 | 142 | /** 143 | * @throws \Throwable 144 | * @return void 145 | * @covers Database::rollBack 146 | */ 147 | public function testRollback(): void 148 | { 149 | $this->db->beginTransaction(); 150 | $this->db->rollBack(); 151 | $this->assertLastQueryEquals('ROLLBACK;'); 152 | } 153 | 154 | /** 155 | * @throws \Throwable 156 | * @return void 157 | * @covers Database::commit 158 | */ 159 | public function testCommit(): void 160 | { 161 | $this->db->beginTransaction(); 162 | $this->db->commit(); 163 | $this->assertLastQueryEquals('COMMIT;'); 164 | } 165 | 166 | /** 167 | * @param string $mode 168 | * @return void 169 | */ 170 | private function assertTransaction(string $mode): void 171 | { 172 | global $wpdb; 173 | $query = $wpdb->queries; 174 | 175 | $firstQuery = reset($query)[0] ?? ''; 176 | $lastQuery = end($query)[0] ?? ''; 177 | $this->assertEquals('START TRANSACTION;', $firstQuery); 178 | $this->assertEquals(0, $this->db->transactionCount); 179 | 180 | if ($mode === 'commit') { 181 | $this->assertEquals('COMMIT;', $lastQuery); 182 | } elseif ($mode === 'rollback') { 183 | $this->assertEquals('ROLLBACK;', $lastQuery); 184 | } 185 | } 186 | 187 | /** 188 | * @return void 189 | */ 190 | private function resetLogQueries(): void 191 | { 192 | global $wpdb; 193 | $wpdb->queries = []; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tests/WordPress/Orm/Schemas/WordPressBuilderTest.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress\Orm\Schemas; 10 | 11 | use Dbout\WpOrm\Orm\Database; 12 | use Dbout\WpOrm\Orm\Schemas\WordPressBuilder; 13 | use Dbout\WpOrm\Tests\WordPress\TestCase; 14 | use Illuminate\Database\Schema\Blueprint; 15 | 16 | class WordPressBuilderTest extends TestCase 17 | { 18 | private Database $database; 19 | private WordPressBuilder $schema; 20 | 21 | /** 22 | * @return void 23 | */ 24 | public static function setUpBeforeClass(): void 25 | { 26 | Database::getInstance()->getSchemaBuilder()->create('project', function (Blueprint $table) { 27 | $table->id(); 28 | $table->string('name'); 29 | $table->integer('author'); 30 | $table->string('address'); 31 | }); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function setUp(): void 38 | { 39 | $this->database = Database::getInstance(); 40 | 41 | /** @var WordPressBuilder $schema */ 42 | $schema = $this->database->getSchemaBuilder(); 43 | $this->schema = $schema; 44 | } 45 | 46 | /** 47 | * @return void 48 | * @covers WordPressBuilder::create 49 | * @covers WordPressBuilder::hasTable 50 | * @covers WordPressBuilder::getColumns 51 | * @covers WordPressBuilder::hasColumn 52 | */ 53 | public function testCreate(): void 54 | { 55 | $this->schema->create('architect', function (Blueprint $table) { 56 | $table->id(); 57 | $table->string('name'); 58 | $table->string('slug'); 59 | $table->json('data')->nullable(); 60 | }); 61 | 62 | $this->assertTrue($this->schema->hasTable('architect')); 63 | $table = $this->database->getTablePrefix() . 'architect'; 64 | $columns = $this->schema->getColumns($table); 65 | $this->assertCount(4, $columns); 66 | $this->assertTrue($this->schema->hasColumn($table, 'id')); 67 | $this->assertTrue($this->schema->hasColumn($table, 'name')); 68 | $this->assertTrue($this->schema->hasColumn($table, 'slug')); 69 | $this->assertTrue($this->schema->hasColumn($table, 'data')); 70 | } 71 | 72 | /** 73 | * @return void 74 | * @covers WordPressBuilder::table 75 | * @covers WordPressBuilder::hasTable 76 | */ 77 | public function testUpdate(): void 78 | { 79 | $this->schema->table('project', function (Blueprint $table) { 80 | $table->string('country'); 81 | $table->boolean('finish'); 82 | }); 83 | 84 | $table = $this->database->getTablePrefix() . 'project'; 85 | $this->assertTrue($this->schema->hasColumn($table, 'country')); 86 | $this->assertTrue($this->schema->hasColumn($table, 'finish')); 87 | } 88 | 89 | /** 90 | * @return void 91 | * @covers WordPressBuilder::drop 92 | */ 93 | public function testDrop(): void 94 | { 95 | $this->schema->create('company', function (Blueprint $table) { 96 | $table->id(); 97 | $table->string('name'); 98 | $table->string('slug'); 99 | }); 100 | 101 | $this->assertTrue($this->schema->hasTable('company')); 102 | $this->schema->drop('company'); 103 | $this->assertFalse($this->schema->hasTable('company')); 104 | } 105 | 106 | /** 107 | * @return void 108 | * @covers WordPressBuilder::dropColumns 109 | */ 110 | public function testDropColumn(): void 111 | { 112 | $this->schema->create('address', function (Blueprint $table) { 113 | $table->id(); 114 | $table->string('firstname'); 115 | $table->string('lastname'); 116 | $table->string('street_1'); 117 | $table->string('street_2'); 118 | $table->string('street_3'); 119 | }); 120 | 121 | $table = $this->database->getTablePrefix() . 'address'; 122 | $columns = $this->schema->getColumns($table); 123 | $this->assertCount(6, $columns); 124 | 125 | $this->schema->dropColumns('address', ['street_3']); 126 | $columns = $this->schema->getColumns($table); 127 | $this->assertCount(5, $columns); 128 | $this->assertFalse($this->schema->hasColumn($table, 'street_3')); 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/WordPress/TestCase.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Dbout\WpOrm\Tests\WordPress; 10 | 11 | use Dbout\WpOrm\Models\Comment; 12 | use Dbout\WpOrm\Models\Post; 13 | use Illuminate\Support\Collection; 14 | 15 | /** 16 | * @method static|$this assertEquals(mixed $expectedValue, mixed $checkValue, string $message = '') 17 | * @method static|$this assertInstanceOf(string $className, mixed $object, string $message = '') 18 | * @method static|$this expectExceptionMessageMatches(string $pattern, string $message = '') 19 | * @method static|$this expectException(string $className, string $message = '') 20 | * @method static|$this assertTrue(mixed $value, string $message = '') 21 | * @method static|$this assertFalse(mixed $value, string $message = '') 22 | * @method static|$this assertNull(mixed $value, string $message = '') 23 | * @method static|$this assertCount(int $expectedCount, array $array, string $message = '') 24 | * @method static|$this assertIsNumeric(mixed $vale, string $message = '') 25 | * @method static|$this assertEqualsCanonicalizing(mixed $expected, mixed $actual, string $message = '') 26 | * @method static mixed factory() 27 | */ 28 | abstract class TestCase extends \WP_UnitTestCase 29 | { 30 | /** 31 | * @param int|null $id 32 | * @return void 33 | */ 34 | public function assertEqualLastInsertId(?int $id): void 35 | { 36 | global $wpdb; 37 | self::assertEquals($wpdb->insert_id, $id); 38 | } 39 | 40 | /** 41 | * @param string $columnName 42 | * @return void 43 | */ 44 | public function expectExceptionUnknownColumn(string $columnName): void 45 | { 46 | $this->expectExceptionMessageMatches(sprintf("/^Unknown column '%s' in 'field list'/", $columnName)); 47 | } 48 | 49 | /** 50 | * @param string $table 51 | * @return string 52 | */ 53 | protected static function getTable(string $table): string 54 | { 55 | global $wpdb; 56 | return $wpdb->prefix . $table; 57 | } 58 | 59 | /** 60 | * @param Post $post 61 | * @return void 62 | */ 63 | public function assertPostEqualsToWpPost(Post $post): void 64 | { 65 | $wpPost = get_post($post->getId()); 66 | 67 | $this->assertInstanceOf(\WP_Post::class, $wpPost); 68 | $this->assertEquals($wpPost->ID, $post->getId()); 69 | $this->assertEquals($wpPost->post_content, $post->getPostContent()); 70 | $this->assertEquals($wpPost->post_type, $post->getPostType()); 71 | $this->assertEquals($wpPost->post_title, $post->getPostTitle()); 72 | $this->assertEquals($wpPost->post_excerpt, $post->getPostExcerpt()); 73 | $this->assertEquals($wpPost->post_name, $post->getPostName()); 74 | } 75 | 76 | /** 77 | * @param Comment $comment 78 | * @return void 79 | */ 80 | public function assertCommentEqualsToWpComment(Comment $comment): void 81 | { 82 | $wpComment = get_comment($comment->getId()); 83 | $this->assertEquals($wpComment->comment_ID, $comment->getId()); 84 | $this->assertEquals($wpComment->comment_content, $comment->getCommentContent()); 85 | $this->assertEquals($wpComment->comment_author_email, $comment->getCommentAuthorEmail()); 86 | $this->assertEquals($wpComment->comment_author, $comment->getCommentAuthor()); 87 | $this->assertEquals($wpComment->comment_type, $comment->getCommentType()); 88 | } 89 | 90 | /** 91 | * @param string $table 92 | * @param string $whereColumn 93 | * @param string $whereValue 94 | * @return void 95 | */ 96 | protected function assertFindLastQuery(string $table, string $whereColumn, string $whereValue): void 97 | { 98 | $this->assertLastQueryEquals( 99 | sprintf( 100 | "select `#TABLE_PREFIX#%s`.* from `#TABLE_PREFIX#%s` where `%s` = '%s' limit 1", 101 | $table, 102 | $table, 103 | $whereColumn, 104 | $whereValue 105 | ) 106 | ); 107 | } 108 | 109 | /** 110 | * @param string $table 111 | * @param string $pkColumn 112 | * @param string $pkValue 113 | * @return void 114 | */ 115 | public function assertLastQueryHasOneRelation(string $table, string $pkColumn, string $pkValue): void 116 | { 117 | $table = sprintf('#TABLE_PREFIX#%s', $table); 118 | $this->assertLastQueryEquals( 119 | sprintf( 120 | "select `%1\$s`.* from `%1\$s` where `%1\$s`.`%2\$s` = %3\$s and `%1\$s`.`%2\$s` is not null limit 1", 121 | $table, 122 | $pkColumn, 123 | $pkValue 124 | ) 125 | ); 126 | } 127 | 128 | /** 129 | * @param string $query 130 | * @param string $message 131 | * @return void 132 | */ 133 | public function assertLastQueryEquals(string $query, string $message = ''): void 134 | { 135 | global $wpdb; 136 | $query = str_replace('#TABLE_PREFIX#', $wpdb->prefix, $query); 137 | self::assertEquals($query, $wpdb->last_query, $message); 138 | } 139 | 140 | /** 141 | * @param Collection $expectedItems 142 | * @param string $relationProperty 143 | * @param array $expectedIds 144 | * @return void 145 | */ 146 | public function assertHasManyRelation( 147 | Collection $expectedItems, 148 | string $relationProperty, 149 | array $expectedIds 150 | ): void { 151 | $ids = $expectedItems->pluck($relationProperty); 152 | $this->assertCount(count($expectedIds), $expectedItems->toArray()); 153 | $this->assertEqualsCanonicalizing($expectedIds, $ids->toArray()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /wp-tests-config-sample.php: -------------------------------------------------------------------------------- 1 |