diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8553979 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,66 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + - beta + - feat/** + +jobs: + phpunit: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + + - name: Install WP-CLI if missing + run: | + if command -v wp >/dev/null 2>&1; then + wp --info + exit 0 + fi + + curl -fsSL -o /tmp/wp-cli.phar https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + chmod +x /tmp/wp-cli.phar + sudo mv /tmp/wp-cli.phar /usr/local/bin/wp + wp --info + + - name: Start test services + run: composer test:env:start + + - name: Install WordPress test environment + run: composer test:install + + - name: Run PHPUnit + run: composer test + + semantic-release-dry-run: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Semantic Release Dry Run + uses: cycjimmy/semantic-release-action@v4 + with: + dry_run: true + extra_plugins: | + @semantic-release/github + @semantic-release/exec + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 22d0d82..23b345f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ vendor +.worktrees +.cache +.phpunit.result.cache diff --git a/.releaserc b/.releaserc index 9053ecc..c09df5f 100644 --- a/.releaserc +++ b/.releaserc @@ -20,7 +20,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "zip -r '/tmp/release.zip' ./src README.md" + "prepareCmd": "zip -r '/tmp/release.zip' src lib composer.json README.md LICENSE" } ], [ diff --git a/README.md b/README.md index 6bfdefe..e3bb658 100644 --- a/README.md +++ b/README.md @@ -1 +1,123 @@ -# WC Data eXtender +
+ +

WC Data Type

+

Model-driven custom data objects for WooCommerce

+ +[![Packagist Version](https://img.shields.io/packagist/v/x-wp/wc-data-type?label=Release&style=flat-square)](https://packagist.org/packages/x-wp/wc-data-type) +![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/x-wp/wc-data-type/php?label=PHP&logo=php&logoColor=white&logoSize=auto&style=flat-square) +![Static Badge](https://img.shields.io/badge/WP-%3E%3D6.9.4-3858e9?style=flat-square&logo=wordpress&logoSize=auto) +![Static Badge](https://img.shields.io/badge/WC-%3E%3D10.6.1-7f54b3?style=flat-square&logo=woocommerce&logoSize=auto) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/x-wp/wc-data-type/tests.yml?label=Tests&event=push&style=flat-square&logo=githubactions&logoColor=white&logoSize=auto)](https://github.com/x-wp/wc-data-type/actions/workflows/tests.yml) + +
+ +This library provides a standardized way to define and work with custom WooCommerce-style data objects. Define a model once, then reuse consistent property handling, factories, queries, and data-store behavior around it. + +## Key Features + +1. Model-driven setup: Define object shape with the `#[Model]` attribute. +2. WooCommerce-style objects: Extend `XWC_Data` and keep a familiar CRUD workflow. +3. Typed properties: Support for core props, meta props, and taxonomy props. +4. Shared object helpers: Load single objects or collections through package utilities. +5. Extensible architecture: Swap in custom data stores, factories, and meta stores when needed. +6. WordPress-native integration: Designed for plugin code already built around WordPress and WooCommerce lifecycles. + +## Installation + +You can install this package via Composer: + +```bash +composer require x-wp/wc-data-type +``` + +> [!TIP] +> We recommend using `automattic/jetpack-autoloader` with this package to reduce autoloading conflicts in WordPress environments. + +## Usage + +Below is a simple example that defines a custom data object and loads it through the package helpers. + +### Defining a model + +```php + array( + 'default' => '', + 'type' => 'string', + ), + 'slug' => array( + 'default' => '', + 'type' => 'slug', + ), + 'price' => array( + 'default' => 0, + 'type' => 'float', + ), + ), +)] +final class Book extends XWC_Data { + protected $object_type = 'book'; +} +``` + +### Loading objects + +```php + 10, + 'orderby' => 'title', + 'order' => 'ASC', + ), +); +``` + +Generated getters and setters follow the declared props, so classes like `Book` can expose methods such as `get_title()`, `set_title()`, `get_slug()`, and `set_price()`. + +## Testing + +The package ships with a PHPUnit suite that boots a WordPress test environment and exercises model definitions, object factories, queries, props, and runtime object behavior. + +Run the suite with: + +```bash +composer test +``` + +To prepare the local WordPress test environment first: + +```bash +composer test:install +composer test +``` + +To clean the local test environment: + +```bash +composer test:clean +``` + +## Documentation + +For package-specific usage, start with the public entrypoints used throughout the library: + +- `XWC\Data\Decorators\Model` +- `XWC_Data` +- `xwc_get_object()` +- `xwc_get_objects()` + +Additional project information is available in the [repository](https://github.com/x-wp/wc-data-type). + +For maintainers preparing prereleases or stable tags, see [docs/release-process.md](docs/release-process.md). diff --git a/bin/clean-wp-tests.sh b/bin/clean-wp-tests.sh new file mode 100755 index 0000000..d9080c5 --- /dev/null +++ b/bin/clean-wp-tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +rm -rf "${REPO_ROOT}/.cache/wp-tests" diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..bf6a5d4 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CACHE_DIR="${REPO_ROOT}/.cache" +WP_CORE_DIR="${WP_CORE_DIR:-${CACHE_DIR}/wp-tests/wordpress}" +WP_TESTS_DIR="${WP_TESTS_DIR:-${CACHE_DIR}/wp-tests/lib}" +WP_DOWNLOADS_DIR="${CACHE_DIR}/wp-tests/downloads" +WP_DEVELOP_DIR="${CACHE_DIR}/wp-tests/wordpress-develop" +PLUGIN_SLUG="xwc-data-type-tests" +PLUGIN_SOURCE_DIR="${REPO_ROOT}/tests/wp-plugin/${PLUGIN_SLUG}" +PLUGIN_TARGET_DIR="${WP_CORE_DIR}/wp-content/plugins/${PLUGIN_SLUG}" + +DB_NAME="${DB_NAME:-wordpress_test}" +DB_USER="${DB_USER:-wordpress}" +DB_PASSWORD="${DB_PASSWORD:-wordpress}" +DB_HOST="${DB_HOST:-127.0.0.1}" +DB_PORT="${DB_PORT:-33067}" +DB_ROOT_USER="${DB_ROOT_USER:-root}" +DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-root}" + +WP_VERSION="${WP_VERSION:-latest}" +WC_VERSION="${WC_VERSION:-latest}" + +log() { + printf '[xwc-tests] %s\n' "$1" >&2 +} + +wp_cli() { + php -d memory_limit=512M "$(command -v wp)" "$@" +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf 'Missing required command: %s\n' "$1" >&2 + exit 1 + fi +} + +wait_for_database() { + log "Waiting for MySQL at ${DB_HOST}:${DB_PORT}" + + for _ in $(seq 1 30); do + if mysqladmin ping \ + --protocol=tcp \ + --host="${DB_HOST}" \ + --port="${DB_PORT}" \ + --user="${DB_ROOT_USER}" \ + --password="${DB_ROOT_PASSWORD}" \ + --silent >/dev/null 2>&1; then + return 0 + fi + + sleep 2 + done + + printf 'Timed out waiting for MySQL.\n' >&2 + exit 1 +} + +ensure_database() { + mysql \ + --protocol=tcp \ + --host="${DB_HOST}" \ + --port="${DB_PORT}" \ + --user="${DB_ROOT_USER}" \ + --password="${DB_ROOT_PASSWORD}" \ + --execute="CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`;" +} + +ensure_wordpress() { + mkdir -p "${WP_CORE_DIR}" "${WP_DOWNLOADS_DIR}" + + if [ ! -f "${WP_CORE_DIR}/wp-load.php" ]; then + log "Downloading WordPress ${WP_VERSION}" + wp_cli core download --version="${WP_VERSION}" --path="${WP_CORE_DIR}" --force + fi + + if [ ! -f "${WP_CORE_DIR}/wp-config.php" ]; then + log 'Creating wp-config.php' + wp_cli config create \ + --path="${WP_CORE_DIR}" \ + --dbname="${DB_NAME}" \ + --dbuser="${DB_USER}" \ + --dbpass="${DB_PASSWORD}" \ + --dbhost="${DB_HOST}:${DB_PORT}" \ + --skip-check \ + --force + fi + + if ! wp_cli core is-installed --path="${WP_CORE_DIR}" >/dev/null 2>&1; then + log 'Installing WordPress site' + wp_cli core install \ + --path="${WP_CORE_DIR}" \ + --url="http://localhost:8080" \ + --title="XWC Data Type Tests" \ + --admin_user="admin" \ + --admin_password="password" \ + --admin_email="admin@example.org" + fi +} + +download_wordpress_develop() { + local wp_core_version="$1" + local archive_path="${WP_DOWNLOADS_DIR}/wordpress-develop-${wp_core_version}.zip" + local extract_root="${WP_DOWNLOADS_DIR}/wordpress-develop-${wp_core_version}" + local source_root + + if [ ! -d "${extract_root}" ]; then + mkdir -p "${WP_DOWNLOADS_DIR}" + + if [ ! -f "${archive_path}" ]; then + log "Downloading wordpress-develop ${wp_core_version}" + if ! curl -fsSL -o "${archive_path}" "https://github.com/WordPress/wordpress-develop/archive/refs/tags/${wp_core_version}.zip"; then + log "Falling back to trunk for wordpress-develop ${wp_core_version}" + curl -fsSL -o "${archive_path}" "https://github.com/WordPress/wordpress-develop/archive/refs/heads/trunk.zip" + fi + fi + + rm -rf "${extract_root}" + mkdir -p "${extract_root}" + unzip -q -o "${archive_path}" -d "${extract_root}" + fi + + source_root="$(find "${extract_root}" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + + if [ -z "${source_root}" ]; then + printf 'Unable to locate extracted wordpress-develop files.\n' >&2 + exit 1 + fi + + printf '%s\n' "${source_root}" +} + +ensure_wordpress_tests_suite() { + local wp_core_version source_root + + mkdir -p "${WP_TESTS_DIR}" + + if [ -f "${WP_TESTS_DIR}/includes/bootstrap.php" ] && [ -f "${WP_TESTS_DIR}/wp-tests-config.php" ]; then + return 0 + fi + + wp_core_version="$(wp_cli core version --path="${WP_CORE_DIR}")" + source_root="$(download_wordpress_develop "${wp_core_version}")" + + log "Preparing WordPress test suite for ${wp_core_version}" + rm -rf "${WP_TESTS_DIR}" + mkdir -p "${WP_TESTS_DIR}" + cp -R "${source_root}/tests/phpunit/." "${WP_TESTS_DIR}/" + + cat > "${WP_TESTS_DIR}/wp-tests-config.php" </dev/null 2>&1 || true +} + +main() { + require_command curl + require_command docker + require_command mysql + require_command mysqladmin + require_command php + require_command unzip + require_command wp + + export WP_CLI_CACHE_DIR="${CACHE_DIR}/wp-cli" + + wait_for_database + ensure_database + ensure_wordpress + ensure_wordpress_tests_suite + ensure_test_plugin + ensure_woocommerce + + log 'WordPress test environment is ready' +} + +main "$@" diff --git a/composer.json b/composer.json index c0e04e5..1c14cc2 100644 --- a/composer.json +++ b/composer.json @@ -21,17 +21,20 @@ "symfony/polyfill-php81": "^1.30", "x-wp/helper-classes": "^1", "x-wp/helper-functions": "^1", - "x-wp/di-implementation": "^1" + "x-wp/di-implementation": "^1", + "symfony/polyfill-php83": "^1.32" }, "require-dev": { "x-wp/di": "^1.0 || ^2.0", "oblak/wordpress-coding-standard": "^1", "php-stubs/wordpress-stubs": "^6.5", + "phpunit/phpunit": "^9.6", "phpstan/extension-installer": "^1.3", "phpstan/phpstan": "^1.10", "phpstan/phpstan-deprecation-rules": "^1.1", "swissspidy/phpstan-no-private": "^0.2.0", - "szepeviktor/phpstan-wordpress": "^1.3" + "szepeviktor/phpstan-wordpress": "^1.3", + "yoast/phpunit-polyfills": "^4.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -41,7 +44,8 @@ "XWC\\Data\\": "src/" }, "classmap": [ - "src/Core/" + "src/Core/", + "src/Interfaces/" ], "files": [ "lib/bootstrap.php", @@ -58,5 +62,15 @@ "phpstan/extension-installer": true }, "platform-check": false + }, + "scripts": { + "test:env:start": "docker compose up -d mysql", + "test:env:stop": "docker compose down --remove-orphans", + "test:install": [ + "@test:env:start", + "bash bin/install-wp-tests.sh" + ], + "test": "vendor/bin/phpunit", + "test:clean": "bash bin/clean-wp-tests.sh" } } diff --git a/composer.lock b/composer.lock index 547943b..0f88e95 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,30 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ba617a63daade3fbd8e55a7b67456bda", + "content-hash": "9c80341551de30f4febf11a40e3223d9", "packages": [ { "name": "automattic/jetpack-constants", - "version": "v2.0.5", + "version": "v3.0.8", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-constants.git", - "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1" + "reference": "f9bf00ab48956b8326209e7c0baf247a0ed721c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1", - "reference": "0c2644d642b06ae2a31c561f5bfc6f74a4abc8f1", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/f9bf00ab48956b8326209e7c0baf247a0ed721c4", + "reference": "f9bf00ab48956b8326209e7c0baf247a0ed721c4", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "automattic/jetpack-changelogger": "^4.2.8", - "brain/monkey": "2.6.1", - "yoast/phpunit-polyfills": "^1.1.1" + "automattic/jetpack-changelogger": "^6.0.5", + "automattic/phpunit-select-config": "^1.0.3", + "brain/monkey": "^2.6.2", + "yoast/phpunit-polyfills": "^4.0.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -36,7 +37,7 @@ "autotagger": true, "mirror-repo": "Automattic/jetpack-constants", "branch-alias": { - "dev-trunk": "2.0.x-dev" + "dev-trunk": "3.0.x-dev" }, "changelogger": { "link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}" @@ -53,22 +54,22 @@ ], "description": "A wrapper for defining constants in a more testable way.", "support": { - "source": "https://github.com/Automattic/jetpack-constants/tree/v2.0.5" + "source": "https://github.com/Automattic/jetpack-constants/tree/v3.0.8" }, - "time": "2024-11-04T09:23:35+00:00" + "time": "2025-04-28T15:12:45+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.2", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "2e1a362527783bcab6c316aad51bf36c5513ae44" + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2e1a362527783bcab6c316aad51bf36c5513ae44", - "reference": "2e1a362527783bcab6c316aad51bf36c5513ae44", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", "shasum": "" }, "require": { @@ -116,7 +117,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-01-24T15:42:37+00:00" + "time": "2025-03-19T13:51:03+00:00" }, { "name": "php-di/invoker", @@ -175,16 +176,16 @@ }, { "name": "php-di/php-di", - "version": "7.0.8", + "version": "7.0.11", "source": { "type": "git", "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "98ddc81f8f768a2ad39e4cbe737285eaeabe577a" + "reference": "32f111a6d214564520a57831d397263e8946c1d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/98ddc81f8f768a2ad39e4cbe737285eaeabe577a", - "reference": "98ddc81f8f768a2ad39e4cbe737285eaeabe577a", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2", + "reference": "32f111a6d214564520a57831d397263e8946c1d2", "shasum": "" }, "require": { @@ -200,8 +201,8 @@ "friendsofphp/php-cs-fixer": "^3", "friendsofphp/proxy-manager-lts": "^1", "mnapoli/phpunit-easymock": "^1.3", - "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^4.6" + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" }, "suggest": { "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" @@ -232,7 +233,7 @@ ], "support": { "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.8" + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11" }, "funding": [ { @@ -244,7 +245,7 @@ "type": "tidelift" } ], - "time": "2025-01-28T21:02:46+00:00" + "time": "2025-06-03T07:45:57+00:00" }, { "name": "psr/container", @@ -301,7 +302,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -357,7 +358,83 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -377,20 +454,20 @@ }, { "name": "x-wp/di", - "version": "v1.7.4", + "version": "v1.8.1", "source": { "type": "git", "url": "https://github.com/x-wp/di.git", - "reference": "21a26e76e405842460ab68f32dd4aedef0deef8a" + "reference": "ab0eed96c4c594c0e6c832f54ebdcbb3ebe74caa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/di/zipball/21a26e76e405842460ab68f32dd4aedef0deef8a", - "reference": "21a26e76e405842460ab68f32dd4aedef0deef8a", + "url": "https://api.github.com/repos/x-wp/di/zipball/ab0eed96c4c594c0e6c832f54ebdcbb3ebe74caa", + "reference": "ab0eed96c4c594c0e6c832f54ebdcbb3ebe74caa", "shasum": "" }, "require": { - "automattic/jetpack-constants": "^2", + "automattic/jetpack-constants": "^2 || ^3", "php": ">=8.0", "php-di/php-di": "^7", "symfony/polyfill-php81": "^1.31", @@ -401,19 +478,23 @@ "oblak/wp-hook-di": "*" }, "provide": { - "psr/container-implementation": "^1.0", + "psr/container-implementation": "1.1 || 2.0", "x-wp/di-implementation": "self.version" }, "require-dev": { + "automattic/jetpack-autoloader": "*", "oblak/wordpress-coding-standard": "^1.1", + "php-stubs/woocommerce-stubs": "^9.5", "php-stubs/wordpress-stubs": "^6.6", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^1.12", "phpstan/phpstan-deprecation-rules": "^1.2", "swissspidy/phpstan-no-private": "^0.2", "symfony/polyfill-php82": "^1.31", + "symfony/var-dumper": "^5.4", "szepeviktor/phpstan-wordpress": "^1.3", - "wp-cli/wp-cli": "^2.11" + "wp-cli/wp-cli": "^2.11", + "x-wp/whoops": "^1.1" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." @@ -452,22 +533,28 @@ ], "support": { "issues": "https://github.com/x-wp/hook-manager/issues", - "source": "https://github.com/x-wp/di/tree/v1.7.4" + "source": "https://github.com/x-wp/di/tree/v1.8.1" }, - "time": "2025-02-05T17:03:52+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-07-06T19:03:40+00:00" }, { "name": "x-wp/helper-classes", - "version": "v1.19.3", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/x-wp/helper-classes.git", - "reference": "a8d3424b875696c87d5dc26e73d66dbbb9992d74" + "reference": "9d0e4611a41846e5407c61548d0de2f7da9c7478" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/helper-classes/zipball/a8d3424b875696c87d5dc26e73d66dbbb9992d74", - "reference": "a8d3424b875696c87d5dc26e73d66dbbb9992d74", + "url": "https://api.github.com/repos/x-wp/helper-classes/zipball/9d0e4611a41846e5407c61548d0de2f7da9c7478", + "reference": "9d0e4611a41846e5407c61548d0de2f7da9c7478", "shasum": "" }, "require": { @@ -508,22 +595,28 @@ ], "support": { "issues": "https://github.com/x-wp/helper-classes/issues", - "source": "https://github.com/x-wp/helper-classes/tree/v1.19.3" + "source": "https://github.com/x-wp/helper-classes/tree/v1.21.0" }, - "time": "2025-02-05T15:40:56+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-02-11T18:24:14+00:00" }, { "name": "x-wp/helper-functions", - "version": "v1.19.3", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/x-wp/helper-functions.git", - "reference": "0430cff023ec47d99ed527501e5189ca799681c5" + "reference": "b6696bc39b68e3df1b9da77c7d4226347861045c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/helper-functions/zipball/0430cff023ec47d99ed527501e5189ca799681c5", - "reference": "0430cff023ec47d99ed527501e5189ca799681c5", + "url": "https://api.github.com/repos/x-wp/helper-functions/zipball/b6696bc39b68e3df1b9da77c7d4226347861045c", + "reference": "b6696bc39b68e3df1b9da77c7d4226347861045c", "shasum": "" }, "require": { @@ -536,6 +629,7 @@ "autoload": { "files": [ "xwp-helper-fns-arr.php", + "xwp-helper-fns-meta.php", "xwp-helper-fns-num.php", "xwp-helper-fns-req.php", "xwp-helper-fns.php" @@ -566,22 +660,28 @@ ], "support": { "issues": "https://github.com/x-wp/helper-functions/issues", - "source": "https://github.com/x-wp/helper-functions/tree/v1.19.3" + "source": "https://github.com/x-wp/helper-functions/tree/v1.21.0" }, - "time": "2025-02-01T19:59:36+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-06-08T13:14:33+00:00" }, { "name": "x-wp/helper-traits", - "version": "v1.19.3", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/x-wp/helper-traits.git", - "reference": "0367d136d6ba36e2ae0fe1854584ef760ea7cae9" + "reference": "c341b8ad27de9b1a73e2d53dc8702897a0cfba18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/x-wp/helper-traits/zipball/0367d136d6ba36e2ae0fe1854584ef760ea7cae9", - "reference": "0367d136d6ba36e2ae0fe1854584ef760ea7cae9", + "url": "https://api.github.com/repos/x-wp/helper-traits/zipball/c341b8ad27de9b1a73e2d53dc8702897a0cfba18", + "reference": "c341b8ad27de9b1a73e2d53dc8702897a0cfba18", "shasum": "" }, "require": { @@ -618,36 +718,42 @@ ], "support": { "issues": "https://github.com/x-wp/helper-traits/issues", - "source": "https://github.com/x-wp/helper-traits/tree/v1.19.3" + "source": "https://github.com/x-wp/helper-traits/tree/v1.21.0" }, - "time": "2024-09-18T12:43:44+00:00" + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-06-08T10:17:19+00:00" } ], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", + "reference": "6e0fa428497bf560152ee73ffbb8af5c6a56b0dd", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, @@ -667,9 +773,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -677,7 +783,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -698,201 +803,529 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-06-27T17:24:01+00:00" }, { - "name": "oblak/wordpress-coding-standard", - "version": "v1.3.0", + "name": "doctrine/instantiator", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/oblakstudio/wordpress-coding-standards.git", - "reference": "b9c6fd5c58edccc464927c9b9f239b0ee0692daf" + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/oblakstudio/wordpress-coding-standards/zipball/b9c6fd5c58edccc464927c9b9f239b0ee0692daf", - "reference": "b9c6fd5c58edccc464927c9b9f239b0ee0692daf", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "ext-filter": "*", - "php": ">=7.4", - "phpcompatibility/php-compatibility": "^9", - "phpcompatibility/phpcompatibility-wp": "^2.1", - "phpcsstandards/phpcsextra": "^1.1", - "phpcsstandards/phpcsutils": "^1.0", - "slevomat/coding-standard": "^8.14", - "squizlabs/php_codesniffer": "^3.8", - "wp-coding-standards/wpcs": "^3" + "php": "^8.1" }, "require-dev": { - "phpcsstandards/phpcsdevtools": "^1.2.0" + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Contributors", - "homepage": "https://github.com/oblakstudio/wordpress-cofing-standard/graphs/contributors" + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" } ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ - "phpcs", - "standards", - "static analysis", - "wordpress" + "constructor", + "instantiate" ], "support": { - "issues": "https://github.com/oblakstudio/wordpress-coding-standards/issues", - "source": "https://github.com/oblakstudio/wordpress-coding-standards/tree/v1.3.0" + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { - "url": "https://github.com/seebeen", - "type": "github" + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" } ], - "time": "2025-04-25T15:40:25+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { - "name": "php-stubs/wordpress-stubs", - "version": "v6.7.1", + "name": "myclabs/deep-copy", + "version": "1.13.4", "source": { "type": "git", - "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/83448e918bf06d1ed3d67ceb6a985fc266a02fd1", - "reference": "83448e918bf06d1ed3d67ceb6a985fc266a02fd1", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^4.13", - "php": "^7.4 || ^8.0", - "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.4.1", - "phpstan/phpstan": "^1.11", - "phpunit/phpunit": "^9.5", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", - "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + "require": { + "php": "^7.1 || ^8.0" }, - "suggest": { - "paragonie/sodium_compat": "Pure PHP implementation of libsodium", - "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "WordPress function and class declaration stubs for static analysis.", - "homepage": "https://github.com/php-stubs/wordpress-stubs", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "PHPStan", - "static analysis", - "wordpress" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.7.1" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, - "time": "2024-11-24T03:57:09+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" }, { - "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "name": "nikic/php-parser", + "version": "v5.7.0", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" - }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + "name": "Nikita Popov" } ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "description": "A PHP parser written in PHP", "keywords": [ - "compatibility", - "phpcs", - "standards" + "parser", + "php" ], "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2019-12-27T09:44:58+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { - "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.3", + "name": "oblak/wordpress-coding-standard", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + "url": "https://github.com/oblakstudio/wordpress-coding-standards.git", + "reference": "b9c6fd5c58edccc464927c9b9f239b0ee0692daf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "url": "https://api.github.com/repos/oblakstudio/wordpress-coding-standards/zipball/b9c6fd5c58edccc464927c9b9f239b0ee0692daf", + "reference": "b9c6fd5c58edccc464927c9b9f239b0ee0692daf", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^9.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "paragonie/random_compat": "dev-master", - "paragonie/sodium_compat": "dev-master" + "ext-filter": "*", + "php": ">=7.4", + "phpcompatibility/php-compatibility": "^9", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpcsstandards/phpcsextra": "^1.1", + "phpcsstandards/phpcsutils": "^1.0", + "slevomat/coding-standard": "^8.14", + "squizlabs/php_codesniffer": "^3.8", + "wp-coding-standards/wpcs": "^3" + }, + "require-dev": { + "phpcsstandards/phpcsdevtools": "^1.2.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/oblakstudio/wordpress-cofing-standard/graphs/contributors" + } + ], + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/oblakstudio/wordpress-coding-standards/issues", + "source": "https://github.com/oblakstudio/wordpress-coding-standards/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://github.com/seebeen", + "type": "github" + } + ], + "time": "2025-04-25T15:40:25+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.8.1", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "92e444847d94f7c30f88c60004648f507688acd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/92e444847d94f7c30f88c60004648f507688acd5", + "reference": "92e444847d94f7c30f88c60004648f507688acd5", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.4", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.1" + }, + "time": "2025-05-02T12:33:34+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" }, "suggest": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", @@ -1021,29 +1454,29 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd" + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/46d08eb86eec622b96c466adec3063adfed280dd", - "reference": "46d08eb86eec622b96c466adec3063adfed280dd", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", + "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.12.1" + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -1099,33 +1532,33 @@ "type": "thanks_dev" } ], - "time": "2025-04-20T23:35:32+00:00" + "time": "2025-06-14T07:40:39+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.12", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -1162,6 +1595,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -1179,244 +1613,1689 @@ "type": "github" }, { - "url": "https://github.com/jrfnl", + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-06-12T04:32:33+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.27", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-05-21T20:51:45+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/f94d246cc143ec5a23da868f8f7e1393b50eaa82", + "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.12" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.1" + }, + "time": "2024-09-11T15:52:35+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.34", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:45:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:22:56+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" } ], - "time": "2024-05-20T13:34:27+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { - "name": "phpstan/extension-installer", - "version": "1.4.3", + "name": "sebastian/recursion-context", + "version": "4.0.6", "source": { "type": "git", - "url": "https://github.com/phpstan/extension-installer.git", - "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", - "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { - "composer-plugin-api": "^2.0", - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0 || ^2.0" + "php": ">=7.3" }, "require-dev": { - "composer/composer": "^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + "phpunit/phpunit": "^9.3" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PHPStan\\ExtensionInstaller\\Plugin" + "branch-alias": { + "dev-master": "4.0-dev" + } }, "autoload": { - "psr-4": { - "PHPStan\\ExtensionInstaller\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "Composer plugin for automatic installation of PHPStan extensions", - "keywords": [ - "dev", - "static analysis" + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, - "time": "2024-09-04T20:21:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" }, { - "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "name": "sebastian/resource-operations", + "version": "3.0.4", "source": { "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" + "phpunit/phpunit": "^9.0" }, "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, - "time": "2025-02-19T13:28:12+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" }, { - "name": "phpstan/phpstan", - "version": "1.12.16", + "name": "sebastian/type", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e0bb5cb78545aae631220735aa706eac633a6be9", - "reference": "e0bb5cb78545aae631220735aa706eac633a6be9", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": ">=7.3" }, - "conflict": { - "phpstan/phpstan-shim": "*" + "require-dev": { + "phpunit/phpunit": "^9.5" }, - "bin": [ - "phpstan", - "phpstan.phar" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, "autoload": { - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", + "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2025-01-21T14:50:05+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "phpstan/phpstan-deprecation-rules", - "version": "1.2.1", + "name": "sebastian/version", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/f94d246cc143ec5a23da868f8f7e1393b50eaa82", - "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "php": ">=7.3" }, - "type": "phpstan-extension", + "type": "library", "extra": { - "phpstan": { - "includes": [ - "rules.neon" - ] + "branch-alias": { + "dev-master": "3.0-dev" } }, "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.1" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, - "time": "2024-09-11T15:52:35+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.18.0", + "version": "8.19.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593" + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/f3b23cb9b26301b8c3c7bb03035a1bee23974593", - "reference": "f3b23cb9b26301b8c3c7bb03035a1bee23974593", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.12.2" + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "phing/phing": "3.0.1", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.13", - "phpstan/phpstan-deprecation-rules": "2.0.2", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-deprecation-rules": "2.0.3", "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.17|12.1.3" + "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -1440,7 +3319,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.18.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" }, "funding": [ { @@ -1452,20 +3331,20 @@ "type": "tidelift" } ], - "time": "2025-05-01T09:40:50+00:00" + "time": "2025-06-09T17:53:57+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.0", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -1536,7 +3415,7 @@ "type": "thanks_dev" } ], - "time": "2025-05-11T03:36:00+00:00" + "time": "2025-06-17T22:17:01+00:00" }, { "name": "swissspidy/phpstan-no-private", @@ -1591,7 +3470,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -1647,7 +3526,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" }, "funding": [ { @@ -1728,6 +3607,56 @@ }, "time": "2024-06-28T22:27:19+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.1.0", @@ -1793,16 +3722,79 @@ } ], "time": "2024-03-25T16:39:00+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "134921bfca9b02d8f374c48381451da1d98402f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/134921bfca9b02d8f374c48381451da1d98402f9", + "reference": "134921bfca9b02d8f374c48381451da1d98402f9", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0 || ^11.0 || ^12.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2025-02-09T18:58:54+00:00" } ], "aliases": [], "minimum-stability": "alpha", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.0" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bac6592 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + mysql: + image: mysql:8.0 + environment: + MYSQL_DATABASE: wordpress_test + MYSQL_USER: wordpress + MYSQL_PASSWORD: wordpress + MYSQL_ROOT_PASSWORD: root + ports: + - "33067:3306" + healthcheck: + test: + - CMD-SHELL + - mysqladmin ping -h 127.0.0.1 -proot --silent + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..edfa798 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,26 @@ +# Release Process + +## Branches + +- `beta` is the prerelease branch. Pushes here may publish prerelease builds only. +- `master` is the stable release branch. Stable tags are cut from commits that land here. + +## v2.0.0 Trigger + +Semantic Release will not infer a major version from test, fix, refactor, or chore commits alone. To cut `v2.0.0`, the merge commit that lands on `master` must include an explicit breaking-change signal: + +- Use a conventional commit subject with `!`, such as `feat!: ship stateful data API`. +- Or include a `BREAKING CHANGE:` footer in the commit body that explains the incompatible change. + +## Release Checklist + +Before merging a stable release to `master`: + +1. Run `composer test` and confirm the full suite passes. +2. Confirm the semantic-release dry-run job passes in GitHub Actions. +3. Verify the release artifact contains `src/`, `lib/`, `composer.json`, `README.md`, and `LICENSE`. +4. Summarize the breaking changes and migration notes in the release notes. + +## Notes + +The GitHub release artifact is intended to be usable outside a Packagist install, so it must include both the runtime source tree and the autoload metadata declared in `composer.json`. diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..53eacbe --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + tests + + + + + + + + diff --git a/src/Core/XWC_Data.php b/src/Core/XWC_Data.php index f67c073..9ff9434 100644 --- a/src/Core/XWC_Data.php +++ b/src/Core/XWC_Data.php @@ -47,6 +47,7 @@ abstract class XWC_Data extends WC_Data implements XWC_Data_Definition { * Data store object. * * @var XWC_Data_Store_XT + * @phpstan-ignore property.phpDocType */ protected $data_store; @@ -74,6 +75,25 @@ public function __construct( int|array|stdClass|XWC_Data $data = 0 ) { ->do_actions( $data ); } + /** + * Get the debug information for this object. + * + * @return array + */ + public function __debugInfo() { + return array( + 'changes' => $this->changes, + 'data' => $this->data, + 'id' => $this->get_id(), + 'meta_data' => wp_list_pluck( + $this->get_meta_data(), + 'value', + 'key', + ), + 'read' => $this->get_object_read(), + ); + } + /** * Universal prop getter / setter * @@ -91,6 +111,36 @@ public function __call( string $name, array $args ): mixed { : $this->set_prop( $prop, $args[0] ); } + /** + * Serialize the object. + * + * @return array{id: int} + */ + public function __serialize(): array { + return array( 'id' => $this->get_id() ); + } + + /** + * Unserialize the object. + * + * @param array{id?: int} $data Data to unserialize. + */ + public function __unserialize( array $data ): void { + $this + ->load_data_store() + ->load_object_args() + ->load_data( $data['id'] ?? 0 ) + ->do_actions( $data['id'] ?? 0 ); + } + + public function jsonSerialize(): mixed { + $data = $this->get_data(); + + unset( $data['meta_data'] ); + + return $data; + } + /** * Load the data for this object from the database. * @@ -127,12 +177,32 @@ public function get_core_data_read(): bool { return (bool) $this->core_read; } + /** + * Take the changes made to the meta props and apply them to the data. + * + * @return void + */ + public function apply_changes() { + $meta_changes = array_intersect( + array_keys( $this->changes ), + array_keys( array_diff_key( $this->data, $this->core_data, $this->extra_data, $this->tax_data ) ), + ); + + foreach ( $meta_changes as $meta_prop ) { + $this->data[ $meta_prop ] = $this->changes[ $meta_prop ]; + unset( $this->changes[ $meta_prop ] ); + } + + parent::apply_changes(); + } + public function save() { $args = $this->get_id() > 0 ? array( 'updated', 'changes' ) : array( 'created', null ); return $this + ->maybe_set_object() ->maybe_set_date( ...$args ) ->save_wc_data(); } @@ -142,7 +212,7 @@ public function save() { * * @param string $name Method name. * @param array $args Method arguments. - * @return array{0: string, 1: string, 2: string}} + * @return array{0: string, 1: string, 2: string} */ final protected function parse_method_name( string $name, array $args ): array { \preg_match( '/^([gs]et)_(.+)$/', $name, $m ); @@ -151,7 +221,7 @@ final protected function parse_method_name( string $name, array $args ): array { $type = $m[1] ?? ''; $prop = $m[2] ?? ''; - if ( ! $method || ! $type || ! $prop || ( 'set' === $type && ! isset( $args[0] ) ) ) { + if ( ! $method || ! $type || ! $prop || ( 'set' === $type && count( $args ) < 1 ) ) { $this->error( 'bmc', \sprintf( 'BMC: %s, %s', static::class, $name ) ); } @@ -328,6 +398,39 @@ protected function is_internal_meta_key( $key ) { return $parent_check; } + protected function maybe_set_object(): static { + if ( ! $this->has_prop_type( 'object' ) ) { + return $this; + } + + $changed = array_diff( + (array) $this->get_prop_by_type( 'object' ), + array_keys( parent::get_changes() ), + ); + + foreach ( $changed as $prop ) { + $obj = $this->{"get_{$prop}"}(); + + if ( ! ( $obj?->changed() ?? false ) ) { + continue; + } + + $this->changes[ $prop ] = $obj; + } + + return $this; + } + + protected function has_prop_type( string $type ): bool { + foreach ( $this->get_prop_types() as $t ) { + if ( $t === $type || str_starts_with( $t, $type . '|' ) ) { + return true; + } + } + + return false; + } + /** * Maybe set the created or updated date on save. * @@ -338,7 +441,7 @@ protected function is_internal_meta_key( $key ) { protected function maybe_set_date( string $type, ?string $key = null ): static { $prop = $this->get_prop_by_type( "date_{$type}" ); - if ( ! $prop ) { + if ( ! $prop || \is_array( $prop ) ) { return $this; } diff --git a/src/Core/XWC_Data_Store_XT.php b/src/Core/XWC_Data_Store_XT.php index e718f5b..fe8d8b1 100644 --- a/src/Core/XWC_Data_Store_XT.php +++ b/src/Core/XWC_Data_Store_XT.php @@ -52,7 +52,7 @@ class XWC_Data_Store_XT extends WC_Data_Store_WP implements WC_Object_Data_Store * core_data: array, * data: array, * tax_data: array, - * prop_types: array, + * prop_types: array, * unique_data: array, * required_data: array, * } @@ -116,7 +116,7 @@ public function get_object_type(): string { * core_data: array, * data: array, * tax_data: array, - * prop_types: array, + * prop_types: array, * unique_data: array, * required_data: array, * } @@ -365,14 +365,14 @@ public function is_value_unique( mixed $value, string $prop, int $obj_id ): bool public function unique_entity_slug( string $slug, string $prop, int $obj_id ): string { global $wpdb; - $prop = $this->get_cols_to_props()[ $prop ] ?? $prop; + $col = $this->get_cols_to_props()[ $prop ] ?? $prop; $check = $wpdb->get_var( $wpdb->prepare( 'SELECT %i FROM %i WHERE %i = %s AND %i != %d LIMIT 1', - $prop, + $col, $this->get_table(), - $prop, + $col, $slug, $this->get_id_field(), $obj_id, @@ -390,7 +390,8 @@ public function unique_entity_slug( string $slug, string $prop, int $obj_id ): s ? \preg_replace( '/-(\d+)$/', "-$suffix", $slug ) : "{$slug}-1"; - return $this->unique_entity_slug( $slug, $prop, $obj_id ); + // Pass the already-remapped column name to avoid double-remapping on recursion. + return $this->unique_entity_slug( (string) $slug, $col, $obj_id ); } /** @@ -467,8 +468,9 @@ protected function get_data_row( int $id ): array { $data_row = $wpdb->get_row( $wpdb->prepare( - "SELECT * FROM %i WHERE {$this->get_id_field()} = %d", + 'SELECT * FROM %i WHERE %i = %d', $this->get_table(), + $this->get_id_field(), $id, ), ARRAY_A, diff --git a/src/Core/XWC_Object_Factory.php b/src/Core/XWC_Object_Factory.php index d02bd1d..95b1285 100644 --- a/src/Core/XWC_Object_Factory.php +++ b/src/Core/XWC_Object_Factory.php @@ -1,6 +1,8 @@ |false get_object_classname(int $id, string $type) Get the object class name. - * @method static int|false get_object_id(mixed $id, string $type) Get the object ID. - * - * Object factory. + * @implements Object_Factory */ -class XWC_Object_Factory { - use Singleton_Ex; - +class XWC_Object_Factory implements Object_Factory { /** - * Array of entity names with their class names. + * Entity type. * - * @var array> + * @var string */ - protected array $models = array(); + protected string $type; /** - * Constructor + * Data object class name. + * + * @var class-string */ - protected function __construct() { - $this->init(); - } + protected string $classname; /** - * Handles dynamic method calls for getting object data. + * Initialize the object factory with an entity. * - * @param string $name Method name. - * @param mixed $args Method arguments. + * @template D of XWC_Data_Store_XT + * @template M of XWC_Meta_Store * - * @return mixed + * @param Entity $e Entity instance. + * @return static */ - public function __call( string $name, $args ) { - \preg_match( '/^get_(.*?)(?:_id|_classname)?$/', $name, $matches ); + public function initialize( Entity $e ): static { + $this->type = $e->name; + $this->classname = $e->model; - $type = $matches[1] ?? ''; - $method = \str_replace( $type, 'object', $name ); + return $this; + } - if ( ! \method_exists( $this, $method ) ) { - return false; + public function make_object( mixed $id ): XWC_Data { + $obj = $this->get_object( $id ); + + if ( $obj ) { + return $obj; } - $args[] = $type; + $classname = $this->get_classname( 0 ); - return $this->{"$method"}( ...$args ); - } + if ( ! $classname ) { + throw new \RuntimeException( + \esc_html( "Cannot resolve a concrete class for entity type '{$this->type}'." ), + ); + } - /** - * Handles dynamic static method calls for getting object data - * - * @param string $name Method name. - * @param mixed $args Method arguments. - * - * @return mixed - */ - public static function __callStatic( $name, $args ) { - return static::instance()->__call( $name, $args ); + return new $classname( 0 ); } - /** - * Get a data object - * - * @param mixed $id Object ID. - * @param string $type Object type. - * @return T|false - */ - public function get_object( mixed $id, string $type = '' ): XWC_Data|bool { - $id = $this->{"get_{$type}_id"}( $id ); + public function get_object( mixed $id ): ?XWC_Data { + $id = $this->get_id( $id ); if ( ! $id ) { - return false; + return null; } - /** - * Filters the class name of a data object. - * - * @var class-string $classname - */ - $classname = $this->{"get_{$type}_classname"}( $id ); + $classname = $this->get_classname( $id ); try { return new $classname( $id ); } catch ( \Exception ) { - return false; - } - } - - /** - * - * Initialize the data types. - * - * @global Entity_Manager $xwc_entities - */ - protected function init(): void { - /** - * Global entity manager. - * - * @var Entity_Manager $xwc_entities - */ - global $xwc_entities; - - foreach ( $xwc_entities->get_entity() as $type => $dto ) { - $this->models[ $type ] = $dto->model; + return null; } } - /** - * Get the object ID. - * - * @param mixed $id Object ID. - * @param string $type Object type. - * @return int|false - */ - protected function get_object_id( mixed $id, string $type ): int|bool { - $obj = $GLOBALS[ $type ] ?? null; + public function get_id( mixed $id ): int|bool { + $obj = $GLOBALS[ $this->type ] ?? null; + // @phpstan-ignore return.type return match ( true ) { - default => false, \is_numeric( $id ) => (int) $id, - $obj instanceof XWC_Data => $obj->get_id(), $id instanceof XWC_Data => $id->get_id(), + $obj instanceof XWC_Data => $obj->get_id(), + default => false, }; } - /** - * Get the object class name. - * - * @param int $id Object ID. - * @param string $type Object type. - * @return class-string|false - */ - protected function get_object_classname( int $id, string $type ): string|bool { + public function get_classname( int $id ): bool|string { /** * Filters the class name of a data object. * * @var class-string|false $classname */ // Documented in WooCommerce. - $classname = \apply_filters( "xwc_{$type}_class", $this->models[ $type ] ?? false, $id ); + $classname = \apply_filters( "xwc_{$this->type}_class", $this->classname, $id ); if ( ! $classname || ! \class_exists( $classname ) ) { return false; diff --git a/src/Core/XWC_Object_Query.php b/src/Core/XWC_Object_Query.php index 0f755d1..7cea3f8 100644 --- a/src/Core/XWC_Object_Query.php +++ b/src/Core/XWC_Object_Query.php @@ -100,10 +100,9 @@ public function count( ?array $query = null ): int { $query['fields'] = 'ids'; $this->query( $query ); - $this->reset(); } - return $this->total; + return $this->total ?? 0; } /** @@ -120,6 +119,7 @@ protected function parse( array $q ): void { 'order' => 'DESC', 'orderby' => $this->id_field, 'page' => 1, + 'per_page' => 20, ); $q = \wp_parse_args( $q, $d ); @@ -262,12 +262,10 @@ protected function init_terms( array &$c, array $q ): void { * @param array $q Query variables. */ protected function init_orderby( array &$c, array $q ): void { - $c['orderby'] = 'ORDER BY '; - $c['orderby'] = match ( $q['orderby'] ) { - 'rand' => 'RAND()', + 'rand' => 'RAND()', $this->id_field => "{$this->table}.{$this->id_field} {$q['order']}", - default => "{$this->table}.{$q['orderby']} {$q['order']}", + default => "{$this->table}.{$q['orderby']} {$q['order']}", }; } diff --git a/src/Core/XWC_Prop.php b/src/Core/XWC_Prop.php new file mode 100644 index 0000000..a9a9472 --- /dev/null +++ b/src/Core/XWC_Prop.php @@ -0,0 +1,331 @@ + + */ +class XWC_Prop implements ArrayAccess, JsonSerializable { + /** + * Traversible data array. + * + * @var array + */ + protected array $data = array(); + + /** + * The hash of the data. + * + * This is used to track changes to the data. + * + * @var string + */ + protected string $hash; + + /** + * Did we read the object? + * + * @var bool + */ + protected bool $read = false; + + protected bool $changed = false; + + /** + * Gets the default JSON representation of the object. + * + * @return static + */ + public static function default(): static { + // @phpstan-ignore new.static + return new static(); + } + + /** + * Constructor. + * + * @param array $data Data and hash. + * } + */ + public function __construct( array $data = array() ) { + $this->data = $this->default_data(); + $this->hash = $this->hash_data(); + + $this->load_data( $data ); + } + + /** + * Serializes the object to an array. + * + * @return array + */ + public function __serialize(): array { + return $this->get_data(); + } + + /** + * Unserializes the object from an array. + * + * @param array $data Data to unserialize. + */ + public function __unserialize( array $data ): void { + $this->load_data( $data ); + } + + /** + * Return data needed for JSON serialization. + * + * @return ?array + */ + public function jsonSerialize(): mixed { + $data = $this->get_data(); + $defl = $this->default_data(); + + $this->sort( $data ); + $this->sort( $defl ); + return $data === $defl + ? null + : $data; + } + + /** + * Sets the value at the specified offset. + * + * @param TKey $offset The offset to set. + * @param TValue $value The value to set. + * @return static + */ + public function set( string $offset, mixed $value ): static { + $old = $this->get( $offset ); + + $this->data[ $offset ] = $value; + + if ( $this->get_read() && $old !== $value ) { + $this->changed = true; + } + + return $this; + } + + /** + * Loads data into the property. + * + * @param array $data The data to load. + * @return static + */ + public function set_data( array $data ): static { + $old = $this->data; + $this->data = $data; + + if ( $this->get_read() && $old !== $data ) { + $this->changed = true; + } + + return $this; + } + + /** + * Mark the object as read. + * + * @param bool $read Whether the object has been read. + * @return static + */ + public function set_read( bool $read = true ): static { + $this->read = $read; + + return $this; + } + + /** + * Sets the data for the property. + * + * @template TData of XWC_Prop + * + * @param TData|array $data The data to set. + * @return ($data is array ? static : TData) + */ + public function with_data( array|XWC_Prop $data ): XWC_Prop { + if ( is_array( $data ) ) { + return $this->set_data( $data ); + } + + $cname = $data::class; + + return new $cname( $data->get_data() ); + } + + /** + * Gets the value at the specified offset. + * + * @param TKey $offset The offset to retrieve. + * @return TValue|null Can return any type or null if not set. + */ + public function get( string $offset ): mixed { + return $this->data[ $offset ] ?? null; + } + + /** + * Gets the data array. + * + * @return array + */ + public function get_data(): array { + return $this->data; + } + + /** + * Gets the hash of the data. + * + * @return ?string + */ + public function get_hash(): ?string { + return $this->hash ?? null; + } + + /** + * Checks if the object has been read. + * + * @return bool + */ + public function get_read(): bool { + return $this->read; + } + + public function changed(): bool { + if ( ! $this->get_read() ) { + return false; + } + + return $this->changed; + } + + /** + * Assigns a value to the specified offset. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to assign the value to. + * @param TValue $value The value to set. + * @return void + */ + public function offsetSet( $offset, $value ): void { + throw new \BadMethodCallException( 'Do not use this method directly. Use the setter method instead.' ); + } + + /** + * Returns the value at the specified offset. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to retrieve. + * @return TValue|array Can return any type. + */ + public function &offsetGet( $offset ): mixed { + return $this->data[ $offset ] ?? array(); + } + + /** + * Checks if the specified offset exists. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to check. + * @return bool + */ + public function offsetExists( $offset ): bool { + return isset( $this->data[ $offset ] ); + } + + /** + * Unsets the value at the specified offset. + * + * Used by the ArrayAccess interface. + * + * @param TKey $offset The offset to unset. + * @return void + */ + public function offsetUnset( $offset ): void { + throw new \BadMethodCallException( 'Do not use this method directly. Use the setter method instead.' ); + } + + /** + * Sets the hash of the data. + * + * @param string $hash The hash to set. + * @return static + */ + protected function set_hash( string $hash ): static { + $this->hash = $hash; + + return $this; + } + + /** + * Returns the default data for this property. + * + * @return array + */ + protected function default_data(): array { + return array(); + } + + /** + * Hashes the data. + * + * This method sorts the data and then hashes it using md5. + * + * @param null|array $data Optional data to hash. If not provided, uses the current data. + * @return string The hash of the data. + */ + protected function hash_data( ?array $data = null ): string { + $data ??= $this->data; + + $this->sort( $data ); + + return hash( 'md5', (string) wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ); + } + + /** + * Sorts the data array recursively. + * + * This method sorts the array in place, preserving the keys and ensuring + * that nested arrays are also sorted. + * + * @param array $arr The array to sort. + * @return void + */ + protected function sort( array &$arr ): void { + array_is_list( $arr ) + ? sort( $arr, SORT_NATURAL | SORT_FLAG_CASE ) + : ksort( $arr, SORT_NATURAL | SORT_FLAG_CASE ); + + foreach ( $arr as &$value ) { + if ( ! is_array( $value ) ) { + continue; + } + + $this->sort( $value ); + } + } + + /** + * Load the data. + * + * @param array $data The data to load. + * } + */ + protected function load_data( array $data = array() ): void { + if ( ! $data ) { + $this->set_read( true ); + return; + } + + $this + ->set_data( $data ) + ->set_hash( $this->hash_data() ) + ->set_read( true ); + } +} diff --git a/src/Decorators/Model.php b/src/Decorators/Model.php index 655e4fb..fd0c893 100644 --- a/src/Decorators/Model.php +++ b/src/Decorators/Model.php @@ -40,14 +40,14 @@ class Model { public string $table; public string $data_store; - public string $factory; + public ?string $factory; /** * Core properties. * * @var array $core_props Array of core properties. * @param array|null $factory Object factory class name. - * @return ($factory is null ? class-string> : class-string) + * @return class-string|null */ - protected function set_factory( ?string $factory ): string { + protected function set_factory( ?string $factory ): ?string { if ( \is_null( $factory ) ) { - return XWC_Object_Factory::class; + return null; } if ( ! \class_exists( $factory ) ) { @@ -312,7 +312,11 @@ protected function set_meta_store( ?string $store ): ?string { return null; } - $store ??= XWC_Meta_Store::class; + if ( \is_null( $store ) ) { + throw new \InvalidArgumentException( + \esc_html( "A concrete meta store class must be provided when meta props are defined for '{$this->name}'." ), + ); + } if ( ! \class_exists( $store ) ) { throw new \InvalidArgumentException( \esc_html( "Meta store class $store does not exist." ) ); @@ -410,7 +414,11 @@ private function parse_tax_arg( array $args ): array { ), ); - $args['field'] = \preg_replace( '/^id$/', 'term_id', \ltrim( $args['field'], 'term_' ) ); + $field = $args['field']; + $field = \str_starts_with( $field, 'term_' ) ? \substr( $field, 5 ) : $field; + $field = 'id' === $field ? 'term_id' : $field; + + $args['field'] = $field; $args['default'] = 'array' === $args['return'] ? (array) $args['default'] : $args['default']; $args['type'] = \sprintf( 'term_%s|%s|%s', $args['return'], $args['field'], $args['taxonomy'] ); diff --git a/src/Decorators/Model_Modifier.php b/src/Decorators/Model_Modifier.php index b893c7a..78d3c4d 100644 --- a/src/Decorators/Model_Modifier.php +++ b/src/Decorators/Model_Modifier.php @@ -28,7 +28,7 @@ class Model_Modifier extends Model { * @param class-string|null $data_store Data store class name. * @param arrayget_definers() as $prop => $setter ) { - $this->$prop = $args[ $prop ] || \is_null( $args[ $prop ] ) - ? $this->$setter( $args[ $prop ] ) - : $args[ $prop ]; + if ( ! \array_key_exists( $prop, $args ) ) { + continue; + } + + $this->$prop = $this->$setter( $args[ $prop ] ); } } } diff --git a/src/Entity.php b/src/Entity.php index c10d762..23eea1a 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -87,6 +87,13 @@ class Entity { 'meta_obj_field', ); + /** + * Properties to be set. + * + * @var array + */ + private static array $props; + /** * Object factories. * @@ -133,7 +140,7 @@ class Entity { */ protected string $container; - protected string $factory; + protected ?string $factory; /** * Core properties. @@ -185,26 +192,26 @@ class Entity { */ protected ContainerInterface $ctr; + /** + * Default values for properties. + * + * @var array + */ + private array $defaults = array( + 'meta_table' => '', + ); + /** * Constructor. * * @param Model ...$defs Model definitions. */ - public function __construct( - Model ...$defs, - ) { - $vars = \array_keys( \get_class_vars( $this::class ) ); - $vars = \array_diff( - $vars, - array( 'args', 'factories', 'stores', 'hooked', 'defaults', 'ctr', 'container' ), - ); - - foreach ( $vars as $var ) { - - $this->$var = $this->set_prop( $var, $defs ); + public function __construct( Model ...$defs ) { + foreach ( $this->get_props() as $prop ) { + $this->$prop = $this->set_prop( $prop, $defs ); } - static::$stores[ $this->name ] = null; + static::$stores[ $this->name ] ??= null; // @phpstan-ignore assign.propertyType } /** @@ -242,7 +249,7 @@ public function add_hooks(): void { * Prime the data store with the entity name. * * @param array $stores Data stores. - * @return array + * @return array */ public function prime_data_store( array $stores ): array { $to_add = \array_keys( static::$stores ); @@ -260,7 +267,19 @@ public function prime_data_store( array $stores ): array { * @return mixed */ protected function set_prop( string $prop, array $defs ): mixed { - $defined = \wp_list_pluck( \wp_list_filter( $defs, array( $prop => null ), 'NOT' ), $prop ); + $defined = array(); + + foreach ( $defs as $def ) { + if ( ! isset( $def->$prop ) ) { + continue; + } + + $defined[] = $def->$prop; + } + + if ( ! \count( $defined ) ) { + return $this->defaults[ $prop ] ?? null; + } if ( 1 === \count( $defined ) ) { return \current( $defined ); @@ -272,9 +291,7 @@ protected function set_prop( string $prop, array $defs ): mixed { return \array_merge( $base, ...$defined ); } - $final = \end( $defined ); - - return $final ? $final : $base; + return \end( $defined ); } /** @@ -292,7 +309,7 @@ protected function get_core_data(): array { * @return array */ protected function get_data(): array { - return \wp_list_pluck( $this->meta_props, 'default' ); + return \wp_list_pluck( $this->meta_props ?? array(), 'default' ); } /** @@ -301,7 +318,7 @@ protected function get_data(): array { * @return array */ protected function get_tax_data(): array { - return \wp_list_pluck( $this->tax_props, 'default' ); + return \wp_list_pluck( $this->tax_props ?? array(), 'default' ); } /** @@ -312,8 +329,8 @@ protected function get_tax_data(): array { protected function get_prop_types(): array { return \array_merge( \wp_list_pluck( $this->core_props, 'type' ), - \wp_list_pluck( $this->meta_props, 'type' ), - \wp_list_pluck( $this->tax_props, 'type' ), + \wp_list_pluck( $this->meta_props ?? array(), 'type' ), + \wp_list_pluck( $this->tax_props ?? array(), 'type' ), ); } @@ -346,7 +363,7 @@ protected function get_required_data(): array { * @return array */ protected function get_meta_to_props(): array { - return \array_flip( \wp_list_pluck( $this->meta_props, 'name' ) ); + return \array_flip( \wp_list_pluck( $this->meta_props ?? array(), 'name' ) ); } /** @@ -364,7 +381,7 @@ protected function get_cols_to_props(): array { * @return array */ protected function get_tax_to_props(): array { - return \array_flip( \wp_list_pluck( $this->tax_props, 'taxonomy' ) ); + return \array_flip( \wp_list_pluck( $this->tax_props ?? array(), 'taxonomy' ) ); } /** @@ -373,7 +390,7 @@ protected function get_tax_to_props(): array { * @return array */ protected function get_tax_fields(): array { - return \wp_list_pluck( $this->tax_props, 'field', 'taxonomy' ); + return \wp_list_pluck( $this->tax_props ?? array(), 'field', 'taxonomy' ); } /** @@ -382,7 +399,14 @@ protected function get_tax_fields(): array { * @return TFact */ protected function get_factory(): XWC_Object_Factory { - return static::$factories[ $this->name ] ??= $this->factory::instance(); + /** + * Variable override. + * + * @var null|class-string $factory + */ + $factory = $this->factory ?? XWC_Object_Factory::class; + + return static::$factories[ $this->name ] ??= $this->make( $factory )->initialize( $this ); } /** @@ -406,7 +430,7 @@ protected function get_meta_store(): ?XWC_Meta_Store { } protected function get_has_meta(): bool { - return '' !== $this->meta_table && array() !== $this->meta_props; + return '' !== $this->meta_table && ! empty( $this->meta_props ); } /** @@ -429,10 +453,22 @@ protected function get_repo(): XWC_Data_Store_XT { return $this->get_data_store(); } + /** + * Get the properties of the entity. + * + * @return array + */ + private function get_props(): array { + return self::$props ??= \array_diff( + \array_keys( \get_class_vars( $this::class ) ), + array( 'props', 'defaults', 'args', 'factories', 'stores', 'hooked', 'ctr', 'container' ), + ); + } + /** * Make an instance of a class. * - * @template TObj of TDstr|TMeta + * @template TObj of TDstr|TMeta|TFact * @param class-string $cname Class name. * @return TObj */ diff --git a/src/Entity_Manager.php b/src/Entity_Manager.php index 2ead7ca..2cb755b 100644 --- a/src/Entity_Manager.php +++ b/src/Entity_Manager.php @@ -115,8 +115,13 @@ protected function do_register( string $classname, ?ContainerInterface $containe * @return array,XWC_Object_Factory,XWC_Meta_Store>> */ protected function get_models( string $target ): array { - $defs = array(); + /** + * Get the inheritance chain for the target class. + * + * @var array> $chain + */ $chain = Reflection::get_inheritance_chain( $target, true ); + $defs = array(); foreach ( $chain as $classname ) { $defs[] = Reflection::get_decorator( $classname, Model::class )?->set_model( $classname ); diff --git a/src/Interfaces/Object_Factory.php b/src/Interfaces/Object_Factory.php new file mode 100644 index 0000000..98fa9a8 --- /dev/null +++ b/src/Interfaces/Object_Factory.php @@ -0,0 +1,44 @@ + + */ + public function get_id( mixed $id ): int|bool; + + /** + * Get the class name of a data object by ID. + * + * @param int $id Object ID. + * @return false|class-string + */ + public function get_classname( int $id ): bool|string; +} diff --git a/src/Interfaces/XWC_Data_Definition.php b/src/Interfaces/XWC_Data_Definition.php index 98950bb..0f2fbab 100644 --- a/src/Interfaces/XWC_Data_Definition.php +++ b/src/Interfaces/XWC_Data_Definition.php @@ -1,4 +1,4 @@ + */ + abstract protected function get_valid_statuses(): array; + + abstract protected function get_default_status(): string; + + abstract protected function get_status_prefix(): string; + + /** + * Set the object status. + * + * @param string $to New status. + * @return array{from: string, to: string} + */ + public function set_status( string $to ): array { + $from = $this->get_status(); + $to = $this->strip_object_status( $to ); + + if ( ! $this->object_read ) { + $this->set_prop( 'status', $to ); + + return \compact( 'from', 'to' ); + } + + if ( ! $this->is_valid_status( $to ) ) { + $to = $this->get_default_status(); + } + + if ( 'draft' !== $from && ! $this->is_valid_status( $from ) ) { + $from = $this->get_default_status(); + } + + $this->set_prop( 'status', $to ); + + return \compact( 'from', 'to' ); + } + + /** + * Get the invoice status. + * + * @param string $context Context. + * @return string + */ + public function get_status( string $context = 'view' ): string { + $status = $this->get_prop( 'status', $context ); + + if ( '' === $status && 'view' === $context ) { + $status = $this->get_default_status(); + } + + return $status; + } + + /** + * Get invoice status prop for the database. + * + * @param string $status Status to format. + * @return string + */ + protected function get_object_status_prop( string $status ) { + if ( '' === $status ) { + $status = $this->get_default_status(); + } + + $status = $this->format_object_status( $status ); + + return $status; + } + + protected function strip_object_status( string $status ): string { + $prefix = $this->get_status_prefix(); + + return \str_starts_with( $status, $prefix ) + ? \substr( $status, \strlen( $prefix ) ) + : $status; + } + + /** + * Format the object status with a prefix. + * + * @param string $status Status to format. + * @return string + */ + protected function format_object_status( string $status ): string { + $prefix = $this->get_status_prefix(); + + return ! \str_starts_with( $status, $prefix ) + ? $prefix . $status + : $status; + } + + /** + * Check if a status is valid. + * + * @param string $status Status to check. + * @return bool + */ + protected function is_valid_status( string $status ): bool { + return \in_array( $this->format_object_status( $status ), $this->get_valid_statuses(), true ); + } +} diff --git a/src/Mixins/Status_Transition_Methods.php b/src/Mixins/Status_Transition_Methods.php new file mode 100644 index 0000000..bd88137 --- /dev/null +++ b/src/Mixins/Status_Transition_Methods.php @@ -0,0 +1,213 @@ +status_transition()->get_id(); + } + + /** + * Check if the object has a specific status. + * + * @param string ...$status Status. + * @return bool + */ + public function has_status( string ...$status ): bool { + return \in_array( $this->get_status(), $status, true ); + } + + /** + * Set the status of the object. + * + * @param string $new_status New status to set. + * @param string $note Note to add to the transition. + * @param bool $manual Whether the status change is manual or not. + * @return array{from: string, to: string} + */ + public function set_status( string $new_status, string $note = '', bool $manual = false ): array { + [ 'from' => $from, 'to' => $to ] = $this->set_status_base( $new_status ); + + if ( ! $this->object_read || '' === $from || $from === $to ) { + return array( + 'from' => $from, + 'to' => $to, + ); + } + + $from = $this->transition['from'] ?? $from; + + $this->transition = array( + 'from' => $from, + 'manual' => $manual, + 'note' => $note, + 'to' => $to, + ); + + if ( $manual ) { + $tag = $this->get_tag_base( 'edit', 'status' ); + + /** + * Fires when the status of an object is manually changed. + * + * @param int $id Object ID. + * @param string $to New status. + * + * @since 1.0.0 + */ + \do_action( $tag, $this->get_id(), $to ); + } + + return array( + 'from' => $from, + 'to' => $to, + ); + } + + /** + * Update the status of the object. + * + * Same as `set_status`, but also saves the object. + * + * @param string $new_status New status to set. + * @param string $note Note to add to the transition. + * @param bool $manual Whether the status change is manual or not. + * @return bool + */ + public function update_status( string $new_status, string $note = '', bool $manual = false ): bool { + if ( ! $this->can_update_status() ) { + return false; + } + + try { + $this->set_status( $new_status, $note, $manual ); + $this->save(); + } catch ( \Throwable ) { + return false; + } + + return true; + } + + protected function can_update_status(): bool { + return $this->get_id() > 0; + } + + /** + * Get the state transition for a specific property. + * + * @return false|array{ + * from: string, + * to: string, + * note: string, + * manual: bool + * } + */ + protected function get_transition(): bool|array { + $transition = $this->transition; + + $this->transition = false; + + return $transition; + } + + protected function status_transition(): static { + $transition = $this->get_transition(); + + if ( false === $transition ) { + return $this; + } + + $base = $this->get_tag_base(); + + try { + + [ 'from' => $from, 'to' => $to ] = $transition; + + /** + * Fires for specific status transition. + * + * @param int $id Object ID. + * @param XWC_Stateful_Data $object Object instance. + * @param array $transition Transition data. + * + * @since 1.0.0 + */ + \do_action( "{$base}_{$to}", $this->get_id(), $this, $transition ); + + /** + * Fires for status transition from one status to another. + * + * @param int $id Object ID. + * @param XWC_Stateful_Data $object Object instance. + * + * @since 1.0.0 + */ + \do_action( "{$base}_{$from}_to_{$to}", $this->get_id(), $this ); + + /** + * Fires for status change. + * + * @param int $id Object ID. + * @param string $from Previous status. + * @param string $to New status. + * @param XWC_Stateful_Data $object Object instance. + * + * @since 1.0.0 + */ + \do_action( "{$base}_changed", $this->get_id(), $from, $to, $this ); + + } catch ( \Exception $e ) { + if ( \function_exists( 'wc_get_logger' ) ) { + $logger = \wc_get_logger(); + + $logger->error( + \sprintf( + 'Status transition of %s #%d errored!', + $this->object_type, + $this->get_id(), + ), + array( + $this->object_type => $this, + 'error' => $e, + ), + ); + } + } finally { + return $this; + } + } + + protected function get_tag_base( string ...$tags ): string { + $tags = $tags ?: array( 'status' ); + \array_unshift( $tags, 'xwc', $this->object_type ); + + return \implode( '_', $tags ); + } +} diff --git a/src/Model/Prop_Getters.php b/src/Model/Prop_Getters.php index 532cd53..576f73a 100644 --- a/src/Model/Prop_Getters.php +++ b/src/Model/Prop_Getters.php @@ -2,6 +2,8 @@ namespace XWC\Data\Model; +use JsonSerializable; +use Stringable; use XWC_Data; /** @@ -29,7 +31,7 @@ trait Prop_Getters { /** * Array linking props to their types. * - * @var array + * @var array */ protected array $prop_types = array(); @@ -48,10 +50,12 @@ trait Prop_Getters { protected array $required_data = array(); public function get_prop_group( string $prop ): string { + $meta_props = \array_diff_key( $this->data, $this->core_data, $this->extra_data, $this->tax_data ); + return match ( true ) { isset( $this->core_data[ $prop ] ) => 'core', isset( $this->extra_data[ $prop ] ) => 'extra', - isset( $this->meta_data[ $prop ] ) => 'meta', + isset( $meta_props[ $prop ] ) => 'meta', default => 'none', }; } @@ -103,7 +107,7 @@ public function get_core_data( string $context = 'db', bool $include_id = false, */ public function get_core_changes(): array { $changed = array(); - $props = \array_intersect( $this->get_core_keys(), \array_keys( $this->changes ) ); + $props = \array_intersect( $this->get_core_keys(), \array_keys( $this->get_changes() ) ); if ( 0 === \count( $props ) ) { return $changed; @@ -133,12 +137,25 @@ public function get_data() { return $data; } + /** + * Get all changes for this object. + * + * This includes core data, extra data, and meta data. + * + * @return array + */ + public function get_changes() { + return $this + ->maybe_set_object() + ->get_wc_data_changes(); + } + /** * Get the type of a prop. * * @param string $prop Name of prop to get type for. * @return array{ - * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'string'|'other', + * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array_set'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, * 1: array * } | array{0: 'enum', 1: array{0: class-string}} */ @@ -148,7 +165,7 @@ protected function get_prop_type( string $prop ): array { /** * Variable narrowing for prop types. * - * @var 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'string'|'other' $type + * @var 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array_set'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string $type */ $type = \array_shift( $types ); @@ -177,7 +194,11 @@ protected function is_base64_string( ?string $value ): bool { * @return ($type is 'date_created' ? null|string : ($type is 'date_updated' ? null|string : null|string|array)) */ protected function get_prop_by_type( string $type ): null|string|array { - $types = \array_filter( $this->get_prop_types(), static fn( $t ) => $t === $type ); + $types = \array_filter( + $this->get_prop_types(), + fn( $t ) => $this->filter_prop( $t, $type ), + ); + $types = \array_keys( $types ); return match ( \count( $types ) ) { @@ -215,6 +236,7 @@ protected function get_prop( $prop, $context = 'view' ) { 'bool_int' => $this->get_bool_prop( $value, 'int' ), 'array_assoc' => $this->get_array_prop( $value, 'assoc' ), 'array' => $this->get_array_prop( $value, 'normal' ), + 'array_set' => $this->get_array_prop( $value, 'set' ), 'term_single' => $this->get_term_prop( $value, ...$sub ), 'term_array' => $this->get_term_prop( $value, ...$sub ), 'enum' => $this->get_enum_prop( $value ), @@ -222,10 +244,23 @@ protected function get_prop( $prop, $context = 'view' ) { 'json_obj' => $this->get_json_prop( $value, \JSON_FORCE_OBJECT ), 'binary' => $this->get_binary_prop( $value ), 'base64_string' => $this->get_base64_string_prop( $value ), + 'object' => $this->get_object_prop( $value, ...$sub ), default => $this->get_unknown_prop( $type, $prop, $value ), }; } + /** + * Get WC data changes. + * + * This is a wrapper for the parent method to ensure that the WC_Data + * changes are returned in the correct format. + * + * @return array + */ + protected function get_wc_data_changes(): array { + return parent::get_changes(); + } + protected function get_wc_data_prop( string $prop, string $context = 'view' ): mixed { return parent::get_prop( $prop, $context ); } @@ -235,7 +270,7 @@ protected function get_date_prop( ?\WC_DateTime $value ): ?string { return null; } - return \gmdate( 'Y-m-d H:i:s', $value->getOffsetTimestamp() ); + return \gmdate( 'Y-m-d H:i:s', $value->getTimestamp() ); } /** @@ -256,7 +291,7 @@ protected function get_bool_prop( mixed $value, string $format = 'string' ): int * Get array prop value. * * @param mixed $value Value to convert to a string. - * @param 'assoc'|'normal' $format Format of the array. + * @param 'assoc'|'normal'|'set' $format Format of the array. * @return string */ protected function get_array_prop( mixed $value, string $format = 'assoc' ): string { @@ -264,6 +299,7 @@ protected function get_array_prop( mixed $value, string $format = 'assoc' ): str // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize 'assoc' => \serialize( $value ), 'normal' => \implode( ',', $value ), + 'set' => \implode( ',', \array_unique( (array) $value ) ), }; } @@ -287,11 +323,11 @@ protected function get_term_prop( mixed $value, string $field, string $taxonomy /** * Get enum prop value. * - * @param \BackedEnum $enum_val Enum value. - * @return string|int + * @param \BackedEnum|null $enum_val Enum value. + * @return null|string|int */ - protected function get_enum_prop( $enum_val ): string|int { - return $enum_val->value; + protected function get_enum_prop( $enum_val ): null|string|int { + return $enum_val?->value ?? null; } /** @@ -312,13 +348,45 @@ protected function get_json_prop( mixed $value, int $flags = 0 ): string { * @return string */ protected function get_binary_prop( mixed $value ): string { - return ! $this->is_binary_string( $value ) ? \hex2bin( $value ) : $value; + if ( $this->is_binary_string( $value ) ) { + return $value; + } + + $decoded = \hex2bin( (string) $value ); + + return false !== $decoded ? $decoded : (string) $value; } - protected function get_base64_string_prop( ?string $value ): string { + protected function get_base64_string_prop( ?string $value ): ?string { + if ( null === $value ) { + return null; + } + return ! $this->is_base64_string( $value ) ? \base64_encode( $value ) : $value; } + /** + * Get an object prop value. + * + * This is used for objects that implement JsonSerializable or Stringable. + * + * @param mixed $value Value to convert to a string. + * @param string $cname Class name of the prop, defaults to XWC_Prop. + * @return ?string + */ + protected function get_object_prop( mixed $value, string $cname = \XWC_Prop::class ): ?string { + $iof = static fn( $t ) => $t instanceof JsonSerializable || $t instanceof Stringable; + $enc = JSON_UNESCAPED_UNICODE; + + return match ( true ) { + $iof( $value ) => $this->get_json_prop( $value, $enc ), + $value === $cname => $this->get_json_prop( new $cname(), $enc ), + \class_exists( (string) $value ) => null, + '' === (string) $value => null, + default => null, + }; + } + protected function get_unknown_prop( string $type, string $prop, mixed $value ): mixed { if ( \method_exists( $this, "get_{$type}_prop" ) ) { $value = $this->{"get_{$type}_prop"}( $value, $prop ); @@ -336,4 +404,10 @@ protected function get_unknown_prop( string $type, string $prop, mixed $value ): */ return \apply_filters( "xwc_data_get_{$type}_prop", $value, $prop ); } + + private function filter_prop( string $type, string $find ): bool { + $regex = '/^' . \preg_quote( $find, '/' ) . '(?:$|\|.+$)/'; + + return 1 === \preg_match( $regex, $type ); + } } diff --git a/src/Model/Prop_Setters.php b/src/Model/Prop_Setters.php index 4c99a77..a754115 100644 --- a/src/Model/Prop_Setters.php +++ b/src/Model/Prop_Setters.php @@ -3,9 +3,11 @@ namespace XWC\Data\Model; use BackedEnum; +use WC_Data_Exception; use XWC_Data; use XWC_Data_Store_XT; use XWC_Meta_Store; +use XWC_Prop; /** * Prop setters trait. @@ -17,20 +19,30 @@ * @phpstan-require-extends XWC_Data */ trait Prop_Setters { + /** + * Whether we are currently inside a set_time_prop → parent::set_date_prop recursion. + * + * Using an instance property instead of a static variable so that concurrent + * calls on different props (or different object instances) cannot interfere. + * + * @var bool + */ + private bool $time_prop_loop = false; + /** * Get the type of a prop. * * @param string $prop Name of prop to get type for. * @return array{ - * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'term_single'|'term_array'|'array_assoc'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'string'|'other', + * 0: 'date_created'|'date_updated'|'date'|'bool'|'bool_int'|'enum'|'term_single'|'term_array'|'array_assoc'|'array_set'|'array'|'binary'|'base64_string'|'json_obj'|'json'|'int'|'float'|'slug'|'other'|string|class-string, * 1: array * } | array{0: 'enum', 1: array{0: class-string}} */ abstract protected function get_prop_type( string $prop ): array; - abstract protected function is_binary_string( string $value ): bool; + abstract protected function is_binary_string( ?string $value ): bool; - abstract protected function is_base64_string( string $value ): bool; + abstract protected function is_base64_string( ?string $value ): bool; /** * Set a collection of props in one go, collect any errors, and return the result. @@ -50,22 +62,15 @@ public function set_props( $props, $context = 'set' ) { return $prop_res; } - $save_res = null; - try { $save_res = $this->save(); } catch ( \Throwable $e ) { - $save_res = new \WP_Error( 'save_error', $e->getMessage() ); - } finally { - return match ( true ) { - 0 === $save_res => new \WP_Error( - 'save_error', - 'An unknown error occurred while saving.', - ), - \is_wp_error( $save_res ) => $save_res, - default => $this, - }; + return new \WP_Error( 'save_error', $e->getMessage() ); } + + return 0 === $save_res + ? new \WP_Error( 'save_error', 'An unknown error occurred while saving.' ) + : $this; } /** @@ -93,9 +98,9 @@ protected function set_prop( $prop, $value ): static { [ $type, $sub ] = $this->get_prop_type( $prop ); match ( $type ) { - 'date_created' => $this->set_date_prop( $prop, $value ), - 'date_updated' => $this->set_date_prop( $prop, $value ), - 'date' => $this->set_date_prop( $prop, $value ), + 'date_created' => $this->set_time_prop( $prop, $value ), + 'date_updated' => $this->set_time_prop( $prop, $value ), + 'date' => $this->set_time_prop( $prop, $value ), 'bool' => $this->set_bool_prop( $prop, $value ), 'bool_int' => $this->set_bool_prop( $prop, $value ), 'enum' => $this->set_enum_prop( $prop, $value, ...$sub ), @@ -103,6 +108,7 @@ protected function set_prop( $prop, $value ): static { 'term_array' => $this->set_array_term_prop( $prop, $value, ...$sub ), 'array_assoc' => $this->set_assoc_arr_prop( $prop, $value ), 'array' => $this->set_normal_arr_prop( $prop, $value ), + 'array_set' => $this->set_unique_arr_prop( $prop, $value ), 'binary' => $this->set_binary_prop( $prop, $value ), 'base64_string' => $this->set_base64_string_prop( $prop, $value ), 'json_obj' => $this->set_json_prop( $prop, $value, false ), @@ -111,6 +117,7 @@ protected function set_prop( $prop, $value ): static { 'float' => $this->set_float_prop( $prop, $value ), 'slug' => $this->set_slug_prop( $prop, $value ), 'string' => $this->set_wc_data_prop( $prop, $value ), + 'object' => $this->set_object_prop( $prop, $value, ...$sub ), default => $this->set_unknown_prop( $type, $prop, $value ), }; @@ -150,17 +157,18 @@ protected function set_wc_data_prop( $prop, $value ) { * @param mixed $value Property value. * @return void */ - protected function set_date_prop( $prop, $value ) { - static $loop; + protected function set_time_prop( $prop, $value ) { + if ( ! $this->time_prop_loop ) { + if ( \is_string( $value ) && \preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value ) ) { + $value = \wc_string_to_timestamp( $value ); + } - if ( ! $loop ) { - $loop = true; + $this->time_prop_loop = true; parent::set_date_prop( $prop, $value ); + $this->time_prop_loop = false; return; } - $loop = false; - $this->set_wc_data_prop( $prop, $value ); } @@ -190,6 +198,11 @@ protected function set_bool_prop( string $prop, $value ) { * @return void */ protected function set_enum_prop( string $prop, mixed $val, null|string|BackedEnum $type = null ) { + if ( null === $type ) { + $this->set_wc_data_prop( $prop, $val ); + return; + } + if ( $val instanceof $type ) { $this->set_wc_data_prop( $prop, $val ); return; @@ -255,6 +268,19 @@ protected function set_normal_arr_prop( string $prop, $value ) { $this->set_wc_data_prop( $prop, \wc_string_to_array( $value ) ); } + /** + * Set an array prop with unique values + * + * @param string $prop Property name. + * @param mixed $value Property value. + * @return void + */ + protected function set_unique_arr_prop( string $prop, $value ) { + $value = \array_values( \array_unique( \wc_string_to_array( $value ) ) ); + + $this->set_wc_data_prop( $prop, $value ); + } + /** * Set an associative array prop * @@ -309,13 +335,41 @@ protected function set_base64_string_prop( string $prop, mixed $value ) { * @return void */ protected function set_json_prop( string $prop, string|array $value, bool $assoc = true ) { - \error_log( 'set_json_prop called with value: ' . \print_r( $value, true ) ); if ( ! \is_array( $value ) ) { $value = \json_decode( $value, $assoc ); } $this->set_wc_data_prop( $prop, $value ); } + /** + * Set an object prop + * + * @template TObj of XWC_Prop + * + * @param string $prop + * @param mixed $value + * @param class-string $cname Class name to parse the value into. + */ + protected function set_object_prop( string $prop, mixed $value, string $cname = XWC_Prop::class ): void { + $data = match ( true ) { + \is_array( $value ) => $value, + \is_string( $value ) => \json_decode( $value, true ) ?? array(), + \is_a( $value, XWC_Prop::class ) => $value, + default => array(), + }; + + /** + * If the object is not read, we need to get the prop from the data store. + * + * @var TObj $obj + */ + $obj = $this->get_object_read() + ? $this->get_prop( $prop )?->with_data( $data ) ?? new $cname( $data ) + : new $cname( $data ); + + $this->set_wc_data_prop( $prop, $obj ); + } + /** * Set an int prop * diff --git a/src/Repo/Query_Handler.php b/src/Repo/Query_Handler.php index e5135aa..d422b10 100644 --- a/src/Repo/Query_Handler.php +++ b/src/Repo/Query_Handler.php @@ -181,15 +181,6 @@ protected function get_date_query_args( array $vars, array $dates ): array { * @return array */ protected function get_meta_query_args( array $vars ): array { - $keys = array( - 'parent', - 'parent_exclude', - 'exclude', - 'limit', - 'type', - 'return', - ); - return parent::get_wp_query_args( $vars ); } diff --git a/src/Utils/xwc-data-utils-object.php b/src/Utils/xwc-data-utils-object.php index 721bc15..5c81a8c 100644 --- a/src/Utils/xwc-data-utils-object.php +++ b/src/Utils/xwc-data-utils-object.php @@ -14,8 +14,14 @@ * @return T */ function xwc_ds( string $name, string $cn = XWC_Data_Store_XT::class ): XWC_Data_Store_XT { + $entity = xwc_get_entity( $name ); + + if ( null === $entity ) { + throw new \RuntimeException( \esc_html( "Entity '{$name}' is not registered." ) ); + } + // @phpstan-ignore return.type - return xwc_get_entity( $name )->repo; + return $entity->repo; } /** @@ -35,7 +41,13 @@ function xwc_data_store( string $name ): WC_Data_Store { * @return XWC_Object_Factory */ function xwc_get_object_factory( string $name ): XWC_Object_Factory { - return xwc_get_entity( $name )->factory; + $entity = xwc_get_entity( $name ); + + if ( null === $entity ) { + throw new \RuntimeException( \esc_html( "Entity '{$name}' is not registered." ) ); + } + + return $entity->factory; } /** @@ -70,18 +82,19 @@ function xwc_get_object( mixed $id, string $name, int|bool|null $def = false ): return $def; } - return xwc_get_object_factory( $name )->{"get_$name"}( $id ) ?: $def; + // @phpstan-ignore return.type + return xwc_get_object_factory( $name )->get_object( $id ) ?: $def; } /** * Get the class name of a data object by ID and type. * * @param int $id Object ID. - * @param string $name Object type. + * @param string $type Object type. * @return class-string */ -function xwc_get_object_classname( int $id, string $name ): string { - return xwc_get_object_factory( $name )->{"get_{$name}_classname"}( $id ); +function xwc_get_object_classname( int $id, string $type ): string { + return xwc_get_object_factory( $type )->get_classname( $id ) ?: XWC_Data::class; } /** @@ -92,9 +105,7 @@ function xwc_get_object_classname( int $id, string $name ): string { * @return XWC_Data */ function xwc_get_object_instance( int $id, string $type ): XWC_Data { - $classname = xwc_get_object_classname( $id, $type ); - - return new $classname( $id ); + return xwc_get_object_factory( $type )->make_object( $id ); } /** diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php new file mode 100644 index 0000000..982dfa3 --- /dev/null +++ b/tests/DataObjectTest.php @@ -0,0 +1,109 @@ +set_name('Alpha'); + $item->set_slug('alpha-item'); + $item->set_score('42'); + + $this->assertSame('Alpha', $item->get_name()); + $this->assertSame('alpha-item', $item->get_slug()); + $this->assertSame(42, $item->get_score()); + } + + public function test_hydrated_item_reports_core_data_and_core_changes(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $item = new XWC_Test_Item(1); + $item->set_score(15); + + $this->assertSame( + array( + 'name' => 'Alpha', + 'slug' => 'alpha', + 'score' => 15, + 'id' => 1, + ), + $item->get_core_data('view', true), + ); + $this->assertSame( + array( + 'score' => 15, + ), + $item->get_core_changes(), + ); + } + + public function test_json_serialize_returns_object_data_without_meta_data_key(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $item = new XWC_Test_Item(1); + $data = $item->jsonSerialize(); + + $this->assertSame(1, $data['id']); + $this->assertSame('Alpha', $data['name']); + $this->assertArrayNotHasKey('meta_data', $data); + } + + public function test_serialization_round_trip_restores_object_identity(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + /** @var XWC_Test_Item $restored */ + $restored = unserialize(serialize(new XWC_Test_Item(1))); + + $this->assertInstanceOf(XWC_Test_Item::class, $restored); + $this->assertSame(1, $restored->get_id()); + $this->assertSame('alpha', $restored->get_slug()); + } + + public function test_object_helpers_return_defaults_and_paged_results(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 30 ), + ), + ); + + $missing = xwc_get_object(999, xwc_test_custom_entity_name(), null); + $paged = xwc_get_objects( + xwc_test_custom_entity_name(), + array( + 'limit' => 2, + 'order' => 'ASC', + 'orderby' => 'score', + 'paginate' => true, + 'return' => 'ids', + ), + ); + + $this->assertNull($missing); + $this->assertSame(array(1, 2), $paged['objects']); + $this->assertSame(2, $paged['pages']); + $this->assertSame(3, $paged['total']); + } +} diff --git a/tests/ModelDefinitionTest.php b/tests/ModelDefinitionTest.php new file mode 100644 index 0000000..f71da86 --- /dev/null +++ b/tests/ModelDefinitionTest.php @@ -0,0 +1,78 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A concrete meta store class must be provided when meta props are defined'); + + new Model( + 'fixture', + '{{PREFIX}}fixture_items', + array( + 'name' => array( + 'default' => '', + 'type' => 'string', + ), + ), + array( + 'color' => array( + 'default' => '', + 'type' => 'string', + ), + ), + ); + } + + public function test_model_normalizes_tax_field_id_to_term_id(): void { + $model = new Model( + 'fixture', + '{{PREFIX}}fixture_items', + array( + 'name' => array( + 'default' => '', + 'type' => 'string', + ), + ), + array(), + array( + 'category' => array( + 'field' => 'id', + 'return' => 'single', + 'taxonomy' => 'category', + ), + ), + ); + + $this->assertSame('term_single|term_id|category', $model->tax_props['category']['type']); + $this->assertSame('term_id', $model->tax_props['category']['field']); + } + + public function test_model_keeps_term_id_tax_field_stable(): void { + $model = new Model( + 'fixture', + '{{PREFIX}}fixture_items', + array( + 'name' => array( + 'default' => '', + 'type' => 'string', + ), + ), + array(), + array( + 'category' => array( + 'field' => 'term_id', + 'return' => 'single', + 'taxonomy' => 'category', + ), + ), + ); + + $this->assertSame('term_single|term_id|category', $model->tax_props['category']['type']); + $this->assertSame('term_id', $model->tax_props['category']['field']); + } +} diff --git a/tests/ObjectFactoryTest.php b/tests/ObjectFactoryTest.php new file mode 100644 index 0000000..ae262e3 --- /dev/null +++ b/tests/ObjectFactoryTest.php @@ -0,0 +1,82 @@ +assertSame(25, $factory->get_id(25)); + $this->assertSame(25, $factory->get_id('25')); + } + + public function test_factory_get_id_accepts_data_objects_and_globals(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $factory = xwc_get_object_factory(xwc_test_custom_entity_name()); + $object = new XWC_Test_Item(1); + + $GLOBALS[xwc_test_custom_entity_name()] = $object; + + $this->assertSame(1, $factory->get_id($object)); + $this->assertSame(1, $factory->get_id(null)); + } + + public function test_factory_get_object_returns_null_for_missing_ids(): void { + $factory = xwc_get_object_factory(xwc_test_custom_entity_name()); + + $this->assertNull($factory->get_object(false)); + } + + public function test_factory_make_object_returns_concrete_object_even_for_zero_id(): void { + $factory = xwc_get_object_factory(xwc_test_custom_entity_name()); + $object = $factory->make_object(0); + + $this->assertInstanceOf(XWC_Test_Item::class, $object); + $this->assertSame(0, $object->get_id()); + } + + public function test_object_classname_falls_back_to_base_class_when_filter_returns_invalid_class(): void { + add_filter( + 'xwc_' . xwc_test_custom_entity_name() . '_class', + static fn (): string => 'Missing_Class', + ); + + try { + $this->assertSame(XWC_Data::class, xwc_get_object_classname(1, xwc_test_custom_entity_name())); + } finally { + remove_all_filters('xwc_' . xwc_test_custom_entity_name() . '_class'); + } + } + + public function test_get_object_instance_returns_hydrated_object(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + ), + ); + + $object = xwc_get_object_instance(1, xwc_test_custom_entity_name()); + + $this->assertInstanceOf(XWC_Test_Item::class, $object); + $this->assertSame('alpha', $object->get_slug()); + } +} diff --git a/tests/PropTest.php b/tests/PropTest.php new file mode 100644 index 0000000..b64d88a --- /dev/null +++ b/tests/PropTest.php @@ -0,0 +1,75 @@ +assertNull($prop->jsonSerialize()); + } + + public function test_set_marks_prop_as_changed_after_read(): void { + $prop = new XWC_Test_Prop(); + + $prop->set('alpha', 'b'); + + $this->assertTrue($prop->changed()); + $this->assertSame('b', $prop->get('alpha')); + } + + public function test_set_data_marks_prop_as_changed_after_read(): void { + $prop = new XWC_Test_Prop(); + + $prop->set_data( + array( + 'alpha' => 'z', + 'items' => array(1, 2, 3), + ), + ); + + $this->assertTrue($prop->changed()); + } + + public function test_with_data_clones_from_other_prop_instances(): void { + $source = new XWC_Test_Prop( + array( + 'alpha' => 'source', + 'items' => array(4, 5), + ), + ); + $clone = XWC_Test_Prop::default()->with_data($source); + + $this->assertInstanceOf(XWC_Test_Prop::class, $clone); + $this->assertNotSame($source, $clone); + $this->assertSame($source->get_data(), $clone->get_data()); + } + + public function test_serialization_round_trip_preserves_data(): void { + $prop = new XWC_Test_Prop( + array( + 'alpha' => 'serialized', + 'items' => array(9, 7), + ), + ); + + /** @var XWC_Test_Prop $roundTrip */ + $roundTrip = unserialize(serialize($prop)); + + $this->assertInstanceOf(XWC_Test_Prop::class, $roundTrip); + $this->assertSame($prop->get_data(), $roundTrip->get_data()); + } + + public function test_array_access_mutators_throw(): void { + $prop = new XWC_Test_Prop(); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Do not use this method directly.'); + + $prop['alpha'] = 'c'; + } +} diff --git a/tests/QueryTest.php b/tests/QueryTest.php new file mode 100644 index 0000000..d6609b5 --- /dev/null +++ b/tests/QueryTest.php @@ -0,0 +1,192 @@ + 1, + 'name' => 'Alpha', + 'slug' => 'alpha', + 'score' => 10, + ), + array( + 'id' => 2, + 'name' => 'Beta', + 'slug' => 'beta', + 'score' => 20, + ), + array( + 'id' => 3, + 'name' => 'Gamma', + 'slug' => 'gamma', + 'score' => 30, + ), + ), + ); + + $query = new XWC_Object_Query( + xwc_test_custom_entity_table_name(), + 'id', + ); + + $this->assertSame( + 3, + $query->count( + array( + 'fields' => 'ids', + 'page' => 1, + 'per_page' => 2, + ), + ), + ); + $this->assertSame(3, $query->total); + } + + public function test_custom_query_can_filter_rows_without_paging(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 20 ), + ), + ); + + $query = new XWC_Object_Query( + xwc_test_custom_entity_table_name(), + 'id', + ); + + $this->assertSame( + array(2, 3), + $query->query( + array( + 'col_query' => array( + 'score' => 20, + ), + 'fields' => 'ids', + 'order' => 'ASC', + 'orderby' => 'id', + 'per_page' => 0, + ), + ), + ); + $this->assertSame(2, $query->total); + $this->assertSame(1, $query->pages); + } + + public function test_xwc_ds_returns_registered_repo_instance(): void { + $repo = xwc_ds(xwc_test_custom_entity_name()); + + $this->assertInstanceOf(XWC_Data_Store_XT::class, $repo); + $this->assertSame(xwc_test_custom_entity_table_name(), $repo->get_table()); + } + + public function test_repo_query_paginates_and_returns_ids(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 30 ), + ), + ); + + $results = xwc_ds(xwc_test_custom_entity_name())->query( + array( + 'limit' => 2, + 'order' => 'ASC', + 'orderby' => 'score', + 'paginate' => true, + 'return' => 'ids', + ), + ); + + $this->assertSame(2, $results['pages']); + $this->assertSame(3, $results['total']); + $this->assertCount(2, $results['objects']); + $this->assertSame(array(1, 2), $results['objects']); + } + + public function test_repo_query_can_return_hydrated_objects(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + ), + ); + + $results = xwc_ds(xwc_test_custom_entity_name())->query( + array( + 'order' => 'ASC', + 'orderby' => 'score', + 'return' => 'objects', + ), + ); + + $this->assertCount(2, $results); + $this->assertInstanceOf(XWC_Test_Item::class, $results[0]); + $this->assertSame('alpha', $results[0]->get_slug()); + $this->assertSame(20, $results[1]->get_score()); + } + + public function test_repo_count_applies_core_column_filters(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + array( 'name' => 'Gamma', 'slug' => 'gamma', 'score' => 20 ), + ), + ); + + $count = xwc_ds(xwc_test_custom_entity_name())->count( + array( + 'score' => 20, + ), + ); + + $this->assertSame(2, $count); + } + + public function test_repo_find_returns_first_matching_object(): void { + xwc_test_seed_custom_entity_rows( + array( + array( 'name' => 'Alpha', 'slug' => 'alpha', 'score' => 10 ), + array( 'name' => 'Beta', 'slug' => 'beta', 'score' => 20 ), + ), + ); + + $found = xwc_ds(xwc_test_custom_entity_name())->find( + array( + 'slug' => 'beta', + ), + ); + + $this->assertInstanceOf(XWC_Test_Item::class, $found); + $this->assertSame(2, $found->get_id()); + $this->assertSame('Beta', $found->get_name()); + } + + public function test_xwc_ds_throws_for_unknown_entities(): void { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('missing-entity'); + + xwc_ds('missing-entity'); + } + + public function test_xwc_get_object_factory_throws_for_unknown_entities(): void { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('missing-entity'); + + xwc_get_object_factory('missing-entity'); + } +} diff --git a/tests/ReleaseConfigTest.php b/tests/ReleaseConfigTest.php new file mode 100644 index 0000000..caaad8f --- /dev/null +++ b/tests/ReleaseConfigTest.php @@ -0,0 +1,41 @@ +assertIsArray($exec_plugin); + $this->assertArrayHasKey('prepareCmd', $exec_plugin); + $this->assertStringContainsString('src', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('lib', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('composer.json', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('README.md', $exec_plugin['prepareCmd']); + $this->assertStringContainsString('LICENSE', $exec_plugin['prepareCmd']); + } + + public function test_release_config_keeps_beta_as_prerelease_branch(): void { + $config = json_decode((string) file_get_contents(dirname(__DIR__) . '/.releaserc'), true, 512, JSON_THROW_ON_ERROR); + + $this->assertSame('master', $config['branches'][0]); + $this->assertContains( + array( + 'name' => 'beta', + 'prerelease' => true, + ), + $config['branches'] + ); + } +} diff --git a/tests/SmokeTest.php b/tests/SmokeTest.php new file mode 100644 index 0000000..2b1e684 --- /dev/null +++ b/tests/SmokeTest.php @@ -0,0 +1,18 @@ +assertTrue(function_exists('add_action')); + $this->assertTrue(class_exists('WooCommerce')); + $this->assertTrue(class_exists(Entity_Manager::class)); + + $manager = Entity_Manager::instance(); + + $this->assertInstanceOf(Entity_Manager::class, $manager); + } +} diff --git a/tests/Support/TestItem.php b/tests/Support/TestItem.php new file mode 100644 index 0000000..73c3db1 --- /dev/null +++ b/tests/Support/TestItem.php @@ -0,0 +1,27 @@ + array( + 'default' => '', + 'type' => 'string', + ), + 'slug' => array( + 'default' => '', + 'type' => 'slug', + ), + 'score' => array( + 'default' => 0, + 'type' => 'int', + ), + ), +)] +final class XWC_Test_Item extends XWC_Data { + protected $object_type = 'xwc_test_item'; +} diff --git a/tests/Support/TestProp.php b/tests/Support/TestProp.php new file mode 100644 index 0000000..2706c1d --- /dev/null +++ b/tests/Support/TestProp.php @@ -0,0 +1,12 @@ + 'a', + 'items' => array(3, 1, 2), + ); + } +} diff --git a/tests/Support/fixtures.php b/tests/Support/fixtures.php new file mode 100644 index 0000000..565f48b --- /dev/null +++ b/tests/Support/fixtures.php @@ -0,0 +1,70 @@ +prefix . 'xwc_test_items'; +} + +function xwc_test_install_custom_entity(): void { + global $wpdb; + + if (! function_exists('dbDelta')) { + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + } + + $table = xwc_test_custom_entity_table_name(); + $charset = $wpdb->get_charset_collate(); + + dbDelta( + "CREATE TABLE {$table} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL DEFAULT '', + slug VARCHAR(200) NOT NULL DEFAULT '', + score BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (id), + KEY slug (slug), + KEY score (score) + ) {$charset};" + ); + + if (! xwc_entity_exists(xwc_test_custom_entity_name())) { + xwc_register_entity(XWC_Test_Item::class); + } +} + +function xwc_test_reset_custom_entity_table(): void { + global $wpdb; + + xwc_test_install_custom_entity(); + $wpdb->query('TRUNCATE TABLE ' . xwc_test_custom_entity_table_name()); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared +} + +/** + * @param array $rows + */ +function xwc_test_seed_custom_entity_rows(array $rows): void { + global $wpdb; + + xwc_test_install_custom_entity(); + + foreach ($rows as $row) { + $wpdb->insert( + xwc_test_custom_entity_table_name(), + wp_parse_args( + $row, + array( + 'name' => '', + 'score' => 0, + 'slug' => '', + ) + ) + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..98e1b13 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,47 @@ +wpdb_table_fix(); + WC_Install::create_tables(); + update_option('woocommerce_version', WC_VERSION); + update_option('woocommerce_db_version', WC()->db_version); + } + }, + 0 + ); + } +); + +require_once $_tests_dir . '/includes/bootstrap.php'; diff --git a/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php b/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php new file mode 100644 index 0000000..ab4732b --- /dev/null +++ b/tests/wp-plugin/xwc-data-type-tests/xwc-data-type-tests.php @@ -0,0 +1,13 @@ +