diff --git a/.docker/.env b/.docker/.env new file mode 100644 index 0000000..b3d80ba --- /dev/null +++ b/.docker/.env @@ -0,0 +1,14 @@ +# Docker only +PROJECT_NAME="event" +PHP_VERSION="7.4" +WP_VERSION="6.0" + +WP_PORT=8888 +DB_PORT=8889 +PMA_PORT=8890 + +# Docker and Codeception +DB_NAME="test" +DB_HOST="mysql" +DB_USER="root" +DB_PASSWORD="root" diff --git a/.docker/codecept b/.docker/codecept new file mode 100755 index 0000000..6a4eb80 --- /dev/null +++ b/.docker/codecept @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Get project name from .env file +PROJECT_NAME=$(grep PROJECT_NAME .env | cut -d '=' -f2 | tr -d '"') + +docker exec -w /var/www/html/wp-content/plugins/"${PROJECT_NAME}" "${PROJECT_NAME}"_test sh -c "vendor/bin/codecept ${*}" \ No newline at end of file diff --git a/.docker/composer b/.docker/composer new file mode 100755 index 0000000..2d5d5da --- /dev/null +++ b/.docker/composer @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Get project name from .env file +PROJECT_NAME=$(grep PROJECT_NAME .env | cut -d '=' -f2 | tr -d '"') + +docker-compose run -w /var/www/html/wp-content/plugins/"${PROJECT_NAME}" --rm wordpress sh -c "composer ${*}" diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml new file mode 100644 index 0000000..df6a603 --- /dev/null +++ b/.docker/docker-compose.yml @@ -0,0 +1,62 @@ +version: '3.7' +services: + wordpress: + build: + context: ./wordpress + args: + PHP_VERSION: ${PHP_VERSION} + WP_VERSION: ${WP_VERSION} + container_name: ${PROJECT_NAME:-wordpress}_test + restart: always + ports: + - ${WP_PORT}:80 + environment: + WORDPRESS_DB_HOST: ${DB_HOST:-mysql} + WORDPRESS_DB_NAME: ${DB_NAME:-test} + WORDPRESS_DB_USER: ${DB_USER:-root} + WORDPRESS_DB_PASSWORD: ${DB_PASSWORD:-root} + WORDPRESS_TABLE_PREFIX: ${TABLE_PREFIX:-wp_} + WORDPRESS_DEBUG: 1 + volumes: + - ../:/var/www/html/wp-content/plugins/${PROJECT_NAME:-wordpress} + - ../tests/_output/:/var/www/html/wp-content/plugins/${PROJECT_NAME:-wordpress}/tests/_output/ + - ./mu-plugins/:/var/www/html/wp-content/mu-plugins/ + depends_on: + - mysql + networks: + integration_test_networks: + + mysql: + image: mysql:${DB_VERSION:-5.7} + container_name: ${PROJECT_NAME:-wordpress}_mysql_test + restart: always + ports: + - ${DB_PORT}:3306 + environment: + MYSQL_DATABASE: ${DB_NAME:-test} + #MYSQL_USER: ${DB_USER:-root} + MYSQL_PASSWORD: ${DB_PASSWORD:-root} + #MYSQL_RANDOM_ROOT_PASSWORD: '1' + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root} + networks: + - integration_test_networks + + phpmyadmin: + depends_on: + - mysql + image: phpmyadmin/phpmyadmin:${PMA_VERSION:-latest} + container_name: ${PROJECT_NAME}_phpmyadmin_test + restart: always + ports: + - ${PMA_PORT}:80 + environment: + # For max upload from PHPMYADMIN https://github.com/10up/wp-local-docker-v2/issues/40#issuecomment-719915040 + UPLOAD_LIMIT: 1G + PMA_HOST: ${DB_HOST:-mysql} + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root} + networks: + - integration_test_networks + +networks: + integration_test_networks: + driver: bridge diff --git a/.docker/wordpress/Dockerfile b/.docker/wordpress/Dockerfile new file mode 100644 index 0000000..f2014b3 --- /dev/null +++ b/.docker/wordpress/Dockerfile @@ -0,0 +1,39 @@ +ARG PHP_VERSION +ARG WP_VERSION + +FROM wordpress:${WP_VERSION}-php${PHP_VERSION} + +# Install pcov for code coverage +RUN set -eux; \ + pecl install pcov; \ + docker-php-ext-enable pcov + +# If you want to use xdebug, uncomment the following lines +# Install xdebug for code coverage +#RUN set -eux; \ +# pecl install xdebug-3.1.4; \ +# docker-php-ext-enable xdebug + +# Set XDEBUG_MODE=coverage or xdebug.mode=coverage +#ENV XDEBUG_MODE=coverage + +RUN set -eux; \ + apt-get update && apt-get install -y \ + git \ + nano \ + less # Needed for the WP-CLI \ + rm -rf /var/lib/apt/lists/* + +# Git add safe directory for the working directory + + +# Needed for Db driver +# https://github.com/Codeception/Codeception/issues/3605 +RUN docker-php-ext-install \ + pdo_mysql + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \ + chmod +x wp-cli.phar && \ + mv wp-cli.phar /usr/local/bin/wp diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 69c8452..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -# editorconfig.org -root = true - -[*.php] -indent_style = tab -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true \ No newline at end of file diff --git a/.env b/.env index e366745..78484b7 100644 --- a/.env +++ b/.env @@ -1,17 +1,12 @@ -WP_ROOT_FOLDER="/tmp/wordpress" -TEST_SITE_WP_ADMIN_PATH="/wp-admin" -TEST_SITE_DB_NAME="test" -TEST_SITE_DB_HOST="localhost" -TEST_SITE_DB_USER="root" -TEST_SITE_DB_PASSWORD="" -TEST_SITE_TABLE_PREFIX="wp_" -TEST_DB_NAME="wploader" -TEST_DB_HOST="localhost" -TEST_DB_USER="root" -TEST_DB_PASSWORD="" -TEST_TABLE_PREFIX="wp_" -TEST_SITE_WP_URL="http://wp.localhost" -TEST_SITE_WP_DOMAIN="wp.localhost" -TEST_SITE_ADMIN_EMAIL="admin@wp.localhost" -TEST_SITE_ADMIN_USERNAME="admin" -TEST_SITE_ADMIN_PASSWORD="password" \ No newline at end of file +# Codeception configuration file +ROOT_FOLDER="../../../" + +DB_HOST="localhost" +DB_NAME="test" +DB_USER="root" +DB_PASSWORD="root" + +TABLE_PREFIX="wp_" + +DOMAIN="localhost" +ADMIN_EMAIL="admin@localhost.test" \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..0dcc2c0 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,37 @@ +name: CS - File lint and file validation + +on: + push: + pull_request: + paths: + - '**workflows/lint.yml' + - '**.php' + - '**phpcs.xml.dist' + - '**composer.json' + +jobs: + lint: + name: ✔ CS check al files + + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Validate php files + run: find ./src/ ./tests/ -type f -name '*.php' -print0 | xargs -0 -L 1 -P 4 -- php -l + + - uses: ramsey/composer-install@v2 + + - name: Coding standard + run: composer run cs \ No newline at end of file diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..15d4c13 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,31 @@ +name: Static Analysis + +on: + pull_request: + push: + paths: + - '**workflows/static-analysis.yml' + - '**.php' + - '**psalm.xml' + - '**composer.json' + +jobs: + tests: + name: Static Analysis for PHP + + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + + - uses: ramsey/composer-install@v2 + + - name: Psalm + run: vendor/bin/psalm \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..623c6c0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,94 @@ +name: CI + +env: + PROJECT_KIND: plugins + DB_HOST: localhost + DB_NAME: test + DB_USER: root + DB_PASSWORD: root + TABLE_PREFIX: wp_ + APP_FOLDER_PATH: /tmp/app + APP_PORT: 8888 + APP_HOST: localhost + APP_USER: root + APP_PASSWORD: root + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + + +jobs: + + tests: + name: 🐘 Tests on PHP ${{matrix.php_versions}} & APP version ${{matrix.app_versions}} + + strategy: + matrix: + php_versions: ['7.4'] + app_versions: ['6.0'] + + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.php_versions == '8.2' }} + if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{matrix.php_versions}} + + - name: Start MySQL + run: | + sudo systemctl start mysql.service + mysql -e "CREATE DATABASE IF NOT EXISTS ${{env.DB_NAME}};" -u${{env.DB_USER}} -p${{env.DB_PASSWORD}} + + - name: Install CLI + run: | + curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + chmod +x wp-cli.phar + sudo mv wp-cli.phar /usr/local/bin/wp + wp cli info + + - name: Create folder for running the App + run: mkdir -p ${{env.APP_FOLDER_PATH}} + + - name: Install App + working-directory: ${{env.APP_FOLDER_PATH}} + run: | + wp core download --version="${{matrix.app_versions}}" + wp config create --dbname="${{env.DB_NAME}}" --dbuser="${{env.DB_USER}}" --dbpass="${{env.DB_PASSWORD}}" --dbhost="${{env.DB_HOST}}" --dbprefix="${{env.TABLE_PREFIX}}" + wp core install --url="${{env.APP_HOST}}:${{env.APP_PORT}}" --title="Test" --admin_user="${{env.APP_USER}}" --admin_password="${{env.APP_PASSWORD}}" --admin_email="${{env.APP_USER}}@${{env.APP_HOST}}.test" --skip-email + wp core update-db + cp -r $GITHUB_WORKSPACE ${{env.APP_FOLDER_PATH}}/wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }} + + - uses: "ramsey/composer-install@v2" + with: + working-directory: "${{env.APP_FOLDER_PATH}}/wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }}" + + - name: Activate ${{ github.event.repository.name }} + working-directory: ${{env.APP_FOLDER_PATH}} + run: | + wp plugin deactivate --all + wp site empty --yes + wp plugin activate ${{ github.event.repository.name }} + wp plugin list --status=active + chmod -R 777 wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }} + ls -la wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }} + wp db export wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }}/tests/_data/dump.sql + + - name: Build codeception + working-directory: ${{env.APP_FOLDER_PATH}}/wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }} + run: ./vendor/bin/codecept build + + - name: Run Unit & Integration test + working-directory: ${{env.APP_FOLDER_PATH}}/wp-content/${{env.PROJECT_KIND}}/${{ github.event.repository.name }} + run: | + ./vendor/bin/codecept run unit --coverage-text + ./vendor/bin/codecept run integration + composer bench diff --git a/.gitignore b/.gitignore index 4dd2a17..b9ec8b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ +# Composer /vendor/ -/_others/ +# Codeception +codeception.yml + +# Rector +rector.php +# Common File *.local -codeception.yml *.lock - -c3.php -infection.log -infection-log.txt -infection.json \ No newline at end of file +*.phar \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bd7beea..0000000 --- a/.travis.yml +++ /dev/null @@ -1,118 +0,0 @@ -# https://www.theaveragedev.com/installing-and-setting-up-wordpress-and-nginx-for-travis-ci-tests/ - -language: php - -notifications: - email: false - -php: - - 7.2 - - nightly - -matrix: - fast_finish: true - allow_failures: - - php: nightly - - env: WP_VERSION=nightly - -services: - - mysql - -cache: - apt: true - directories: - - vendor - - $HOME/.composer/cache/files - -addons: - apt: - packages: - - libjpeg-dev - - libpng12-dev - - php7.0-fpm - - php7.0-mysql - - nginx - hosts: - - wp.localhost - -env: - global: - - WP_FOLDER="/tmp/wordpress" - - WP_URL="http://wp.localhost" - - WP_DOMAIN="wp.localhost" - - DB_NAME="test" - - TEST_DB_NAME="wploader" - - WP_TABLE_PREFIX="wp_" - - WP_ADMIN_USERNAME="admin" - - WP_ADMIN_PASSWORD="admin" - matrix: - - WP_VERSION=latest - - WP_VERSION=nightly - -before_install: - # create the databases that will be used in the tests - - mysql -e "create database IF NOT EXISTS $DB_NAME;" -uroot - - mysql -e "create database IF NOT EXISTS $TEST_DB_NAME;" -uroot - # set up folders - - mkdir -p $WP_FOLDER - - mkdir tools - # install wp-cli in the `tools` folder - - wget https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -P $(pwd)/tools/ - - chmod +x tools/wp-cli.phar && mv tools/wp-cli.phar tools/wp - # append the `tools` folder to the PATH - - export PATH=$PATH:$(pwd)/tools - # prepend the `vendor/bin` folder the PATH - - export PATH=vendor/bin:$PATH - -install: - - | - if [ $TRAVIS_PHP_VERSION == "nightly" ]; then - composer install --prefer-dist --ignore-platform-reqs - else - composer install --prefer-dist - fi - # install WordPress - - cd $WP_FOLDER - - wp core download --version=$WP_VERSION - - wp config create --dbname="$DB_NAME" --dbuser="root" --dbpass="" --dbhost="127.0.0.1" --dbprefix="$WP_TABLE_PREFIX" - - wp core install --url="$WP_URL" --title="Test" --admin_user="$WP_ADMIN_USERNAME" --admin_password="$WP_ADMIN_PASSWORD" --admin_email="admin@$WP_DOMAIN" --skip-email - - wp rewrite structure '/%postname%/' --hard - # update WordPress database to avoid prompts - - wp core update-db - # copy the plugin in the WordPress plugin folder - - cp -r $TRAVIS_BUILD_DIR $WP_FOLDER/wp-content/plugins/event - # show the plugins folder contents to make sure the plugin folder is there - - ls $WP_FOLDER/wp-content/plugins - # activate the plugin - - wp plugin activate event - # make sure the plugin is active on the site - - wp plugin list --status=active - # export a dump of the just installed database to the _data folder - - wp db export $TRAVIS_BUILD_DIR/tests/_data/dump.sql - # get back to the build folder - - cd $TRAVIS_BUILD_DIR - # open up the site folder to allow the PHP application to read/write/execute on it - - sudo chmod -R 777 $WP_FOLDER - # copy the Nginx configuration file to the available sites - #- sudo cp build/travis-nginx-conf /etc/nginx/sites-available/$WP_DOMAIN - #- sudo sed -e "s?%WP_FOLDER%?$WP_FOLDER?g" --in-place /etc/nginx/sites-available/$WP_DOMAIN - #- sudo sed -e "s?%WP_DOMAIN%?$WP_DOMAIN?g" --in-place /etc/nginx/sites-available/$WP_DOMAIN - # enable the site - - sudo ln -s /etc/nginx/sites-available/$WP_DOMAIN /etc/nginx/sites-enabled/ - -before_script: - # restart Nnginx and PHP-FPM services - - sudo service php7.0-fpm restart - - sudo service nginx restart - - # build Codeception modules - - codecept build - -script: - - find ./src/ ./tests/ -type f -name '*.php' -print0 | xargs -0 -L 1 -P 4 -- php -l - - vendor/bin/phpcs --ignore=./tests/_support/* ./src/ ./tests/ - - vendor/bin/phpstan analyze - - vendor/bin/psalm - - codecept run wpunit && codecept run unit --coverage --coverage-text -# - vendor/bin/infection --threads=4 -# - vendor/bin/phpbench run --report=performance diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..961a1c3 --- /dev/null +++ b/Makefile @@ -0,0 +1,184 @@ +DOCKER_FOLDER = .docker +DOCKER_DIR = cd $(DOCKER_FOLDER) && +HOST_OWNER = $(shell id -u):$(shell id -g) +FILES_OWNERSHIP = sudo chown -R $(HOST_OWNER) . + +default: help + +# https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +# https://gist.github.com/prwhite/8168133?permalink_comment_id=4266839#gistcomment-4266839 +.PHONY: help +help: ## Display this help screen + @grep -hP '^\w.*?:.*##.*$$' $(MAKEFILE_LIST) | sort -u | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: files/mod +files/permission: ### Set the executable files in Docker folder permissions to 777 + @echo "Checking files permissions" + @find $(DOCKER_FOLDER) -maxdepth 1 -type f -exec grep -lE '^#!' {} \; | xargs chmod 777 + @find $(DOCKER_FOLDER) -maxdepth 1 -type f -exec grep -lqE '^#!' {} \; -exec ls -la {} \; + @echo "Files permissions ok" + +.PHONY: files/own +files/own: ### Set again the ownership off all the file to the current user + @echo "Setting the ownership of all the files to the current user" + @$(FILES_OWNERSHIP) + @echo "Ownership set" + +# Docker commands + +.PHONY: build +build: files/permission ### Build the containers inside the ./docker folder + @echo "Building the containers" + $(DOCKER_DIR) docker-compose up -d --build --remove-orphans + @echo "Containers built" + +.PHONY: up +up: files/permission ### Start the containers inside the ./docker folder + @echo "Starting the containers..." + @$(DOCKER_DIR) docker-compose up -d --remove-orphans + @echo "Containers started" + +.PHONY: down +down: ### Stop the containers inside the ./docker folder + @echo "Stopping the containers" + @$(DOCKER_DIR) docker-compose down --remove-orphans --volumes + @echo "Containers stopped" + +# Composer commands + +.PHONY: composer/install +composer/install: up ### Install the composer dependencies + @echo "Installing the composer dependencies" + @$(DOCKER_DIR) ./composer install + +.PHONY: composer/update +composer/update: up ### Update the composer dependencies + @echo "Updating the composer dependencies" + @$(DOCKER_DIR) ./composer update + +.PHONY: composer/dump +composer/dump: up ### Dump the composer autoload + @echo "Dumping the composer autoload" + @$(DOCKER_DIR) ./composer dump-autoload + +# Codestyle commands + +.PHONY: cs +cs: up ### Run the code sniffer + @echo "Running the code sniffer" + @$(DOCKER_DIR) ./composer cs + +.PHONY: cs/fix +cs/fix: up ### Run the code sniffer and fix the errors + @echo "Running the code sniffer and fix the errors" + @$(DOCKER_DIR) ./composer cs:fix + +# Psalm commands + +.PHONY: psalm +psalm: up ### Run the psalm + @echo "Running the psalm" + @$(DOCKER_DIR) ./composer psalm + +# Codeception commands + +.PHONY: codecept/build +codecept/build: up ### Build the codeception suites + @echo "Building the codeception suites" + @$(DOCKER_DIR) ./codecept build + @$(FILES_OWNERSHIP) + +.PHONY: clean +clean: up ### Clean the codeception suites + @echo "Cleaning the codeception suites" + @$(DOCKER_DIR) ./codecept clean + +.PHONY: unit +unit: up ### Run the unit tests + @echo "Running the unit tests" + @$(DOCKER_DIR) ./codecept run unit --debug + +.PHONY: integration +integration: up ### Run the integration tests + @echo "Running the integration tests" + @$(DOCKER_DIR) ./codecept run integration --debug + +.PHONY: functional +functional: up ### Run the functional tests + @echo "Running the functional tests" + @$(DOCKER_DIR) ./codecept run functional --debug + +.PHONY: acceptance +acceptance: up ### Run the acceptance tests + @echo "Running the acceptance tests" + @$(DOCKER_DIR) ./codecept run acceptance + +.PHONY: tests +tests: unit integration ### Run unit and integration tests + +.PHONY: qa +qa: cs psalm unit integration ### Run all the tests + +# Infection commands + +.PHONY: infection +infection: up ### Run the infection + @echo "Running the infection" + @$(DOCKER_DIR) ./composer infection + +# Rector commands + +.PHONY: rector +rector: up ### Run the rector with dry-run + @echo "Running the rector in dry-run mode, if you want to apply the refactorings run make rector/fix" + @$(DOCKER_DIR) ./composer rector + +.PHONY: rector/fix +rector/fix: up ### Apply the rector refactorings + @echo "Running the rector" + @$(DOCKER_DIR) ./composer rector:fix + @$(FILES_OWNERSHIP) + +# Benchmark commands + +.PHONY: bench +bench: ### Run the benchmark in the local machine not in the docker container + @echo "Running the benchmark" + @composer bench + +# PhpMetrics commands + +.PHONY: docker/metrics +docker/metrics: ### Run the phpmetrics + @echo "Running the phpmetrics" + @docker run --rm \ + --user $(id -u):$(id -g) \ + --volume `pwd`:/project \ + herloct/phpmetrics --report-html=./tests/_output/report src + +# PhpMetrics commands from composer + +.PHONY: metrics +metrics: up ### Run the composer/metrics + @echo "Running the psalm" + @$(DOCKER_DIR) ./composer metrics + +# Generate commands + +.PHONY: generate +generate: up ### Run the codeception generate test files in unit and integration suite, use FILE=filename to generate a specific file + @echo "Start generating the test files" + @if [ ! -z "$(FILE)" -a ! -f "tests/unit/$(FILE)Test.php" ]; then \ + echo "File does not exists, generating now."; \ + ./vendor/bin/codecept generate:test unit $(FILE); \ + else \ + echo "FILE variable empty or file already generated"; \ + fi + @if [ ! -z "$(FILE)" -a ! -f "tests/integration/$(FILE)Test.php" ]; then \ + echo "File does not exists, generating now."; \ + ./vendor/bin/codecept generate:wpunit integration $(FILE); \ + else \ + echo "FILE variable empty or file already generated"; \ + fi + @$(FILES_OWNERSHIP) + @echo "Files generated" diff --git a/README.md b/README.md index 12d2c38..66e2760 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,27 @@ ![PHP from Packagist](https://img.shields.io/packagist/php-v/italystrap/event) ![Scrutinizer code quality (GitHub/Bitbucket)](https://img.shields.io/scrutinizer/quality/g/ItalyStrap/event?label=Scrutinizer) -WordPress Hooks Events plus Psr-14 Events API the OOP way +PSR-14 Event Dispatcher implementation for WordPress and wrappers around the WordPress Plugin API (Events aka Hooks) **It is still a WIP** +Please, even if it works very well, also the PSR-14 implementation works very well, keep in mind that this is still a WIP until this package reach the version 1.x.x, for now it is a 0.x.x version (if you don't know what this means, please read the [SemVer](http://semver.org/) specification). + +Personally I'm very proud of this package (like I'm proud for all the others 😊), at the end it was not very complicated to implement the PSR-14 standard, but I needed to hack a little bit the WordPress Plugin API to make it work as expected. + +BTW, I'm using this package in production for my own projects and right now I have no issue, if you find some please, please, let me know opening an issue (or a PR if you want to fix it), here the link to the [issue tracker](https://github.com/ItalyStrap/event/issues). + +The naming convention I used is: when you encounter the word `Global*` means that it is something related to the WordPress only because WP use global variables under the hood and I didn't want to use some prefix WP related, even if it is also for WordPress I never like prefixing my code with something related to a word that contains some correlation with it. + ## Table Of Contents * [Installation](#installation) +* [Introduction](#introduction) * [Basic Usage](#basic-usage) * [Advanced Usage](#advanced-usage) * [Contributing](#contributing) * [License](#license) +* [Credits](#credits) ## Installation @@ -29,31 +39,183 @@ composer require italystrap/event ``` This package adheres to the [SemVer](http://semver.org/) specification and will be fully backward compatible between minor versions. +## Introduction + +Welcome to the documentation for ItalyStrap Event! In this introductory section, we will provide you with an overview of event-driven programming in the context of PHP development, WordPress and highlight the benefits of adopting this approach. + +### Event-Driven Programming: An Overview + +Event-driven programming is a paradigm widely used in software development to handle reactive scenarios and manage interactions within a software system. It revolves around the concept of events, which represent specific occurrences or interactions. + +In event-driven programming, software components (listeners or subscribers) register their interest in specific events and define how they should respond when those events occur. This decoupled and reactive architecture promotes modularity, flexibility, and maintainability in complex applications. +Advantages of Event-Driven Programming + +Event-driven programming offers several advantages that make it a valuable approach in various software development contexts: + +#### 1. Loose Coupling +Events act as communication channels between different parts of a system, allowing them to interact without tight dependencies. This loose coupling enhances code reusability and promotes the separation of concerns. + +#### 2. Scalability +Event-driven systems can effectively handle a large number of concurrent events, ensuring that the system remains responsive and adaptable to varying workloads. + +#### 3. Flexibility +Event-driven architectures are flexible and extensible. New functionality can be added by introducing new events and listeners without major changes to existing code. + +#### 4. Testability +Isolating event listeners allows for easier unit testing, as you can focus on testing individual components without the need for complex integration testing. + +In the following sections of this documentation, we will delve deeper into the specifics of event-driven programming within the PHP ecosystem. We will explore how to work with events in WordPress, the core APIs for event handling, and how to seamlessly integrate the PSR-14 standard into your projects. + +Let's continue our journey into the world of event-driven programming! + +## How an Event-Driven System Works + +In this section, we'll provide an overview of how an event-driven system operates, explaining fundamental concepts, highlighting key components, and offering examples of common use cases for event-driven systems. + +### Understanding Event-Driven Programming + +Event-driven programming is a software architecture that relies on events to trigger and manage the flow of a program. Here are some key concepts to grasp: + +#### Events + +Events represent specific occurrences or interactions within a system. They serve as signals or notifications that something has happened or needs attention. Events can range from user actions (e.g., button clicks) to system-generated notifications (e.g., data updates). + +#### Event Handlers (Listeners) + +Event handlers, often referred to as listeners or subscribers, are components responsible for responding to specific events. These listeners register their interest in particular events and execute predefined actions when those events occur. + +#### Event Loop + +The event loop is a fundamental part of event-driven systems. It continuously monitors for events and dispatches them to the appropriate event handlers. The loop ensures that events are processed in the order they occur, providing a responsive and non-blocking execution environment. + +### Key Components of an Event-Driven System + +An event-driven system typically consists of the following components: + +#### 1. Events + +Events define what can happen in the system and encapsulate relevant data associated with those occurrences. + +#### 2. Event Handlers (Listeners) + +Event handlers, or listeners, respond to specific events by executing the corresponding actions or functions. + +#### 3. Event Loop + +The event loop manages the flow of events, ensuring they are dispatched to the correct listeners. + +#### 4. Dispatcher + +The dispatcher is responsible for coordinating the dispatch of events to their respective listeners. It acts as the central hub for event handling. + +### Common Use Cases for Event-Driven Systems + +Event-driven systems are versatile and find applications in various domains. Here are some common use cases: + +### 1. User Interfaces + +Graphical user interfaces (GUIs) often rely on event-driven architectures to respond to user interactions such as button clicks, mouse movements, and keyboard inputs. + +#### 2. Real-Time Applications + +Systems requiring real-time processing, such as online games, chat applications, and financial trading platforms, benefit from event-driven designs to ensure responsiveness. + +#### 3. Notifications and Alerts + +Event-driven systems are well-suited for delivering notifications and alerts based on specific triggers or conditions. + +#### 4. IoT (Internet of Things) + +IoT applications use event-driven principles to manage and process data generated by connected devices and sensors. + +In the upcoming sections, we'll explore how event-driven programming is implemented in WordPress, dive into the core APIs for event handling, and demonstrate how to seamlessly integrate the PSR-14 standard into your projects. + +Let's continue our journey into the world of event-driven programming! + +## How the WordPress Event System Works: Core APIs + +In this section, we will dive into the core APIs used for event handling in WordPress. These APIs are essential for managing actions and filters, which serve as the building blocks of WordPress event-driven architecture. +Unfortunately WordPress Event API use global variables under the hood, and this is a bad practice, but we can't do anything about it because WordPress will never change this, so we need to live with it and close our nose. + +### Actions and Filters + +#### Actions + +Actions in WordPress are events triggered at specific points during the execution of a request. Actions are instrumental for executing side effects or custom code at predefined moments within the WordPress lifecycle. + +Actions do not return any values; instead, they serve as a signal for event handlers to perform tasks. When an action is fired, all attached action handlers (known as "action hooks") are executed sequentially. + +Example of dispatching a simple action event (hook) without any arguments in WordPress: + +```php +\do_action('my_custom_action'); +``` + +And here the same event as above but with arguments this time: + +```php +\do_action('my_custom_action', $arg1, $arg2); +``` + +Here, 'my_custom_action' represents the event name or hook. WordPress provides numerous predefined action hooks that developers can leverage to extend and customize the platform. +To naming a few registered in the core: + +* `init` +* `wp_loaded` +* `admin_init` +* `admin_menu` +* and so on... + +#### Filters + +Filters in WordPress are events similar to actions but with an important distinction: filters allow modification of data before it is used elsewhere in the system. Filters are used when you want to modify a value, content, or data passed through the filter (yes, you could also do this with actions, but I will talk about it later). + +Filters return a modified or unaltered value, always, and multiple filter handlers (listener) can be applied in sequence as you can do with actions. Filters are widely used for customizing and manipulating data within WordPress. + +Example of defining a filter hook and applying a filter: + +```php +$data_to_modify = 'Some data to modify'; +$filtered_data = \apply_filters('my_custom_filter', $data_to_modify); +``` + +Here, 'my_custom_filter' denotes the filter's event name or hook. WordPress allows developers to create their custom filters in addition to utilizing predefined filters. + +Understanding the concept of hook/event names is crucial when working with actions and filters in WordPress. Event names serve as identifiers for specific points in the execution flow where custom code can be attached. Developers can use both WordPress-defined hooks and create custom hooks to extend and customize WordPress functionality. + +So to remember **actions** and **filters** are mostly the same thing, and they are the **dispatcher** of the event system in WordPress. + +WordPress use string as event name and in the WordPress documentation they are called **hooks**. + +**Dangerous things to know about dispatching actions and filters:** +Never ever dispatch an event inside a constructor of a class, this is a very bad practice and if you do that you are a bad developer. + +[🆙](#table-of-contents) + ## Basic Usage -The `EventDispatcher::class` is a wrapper around the (WordPress Plugin API)[https://developer.wordpress.org/plugins/hooks/] +The `\ItalyStrap\Event\GlobalDispatcher::class`, `\ItalyStrap\Event\GlobalOrderedListenerProvider::class` and `\ItalyStrap\Event\GlobalState::class` are wrappers around the (WordPress Plugin API)[https://developer.wordpress.org/plugins/hooks/] + +The `\ItalyStrap\Event\Dispatcher::class` and `\ItalyStrap\Event\GlobalOrderedListenerProvider::class` implement the (PSR-14)[https://www.php-fig.org/psr/psr-14/] Event Dispatcher. ### Simple example for actions ```php -use ItalyStrap\Event\EventDispatcher; - -$dispatcher = new EventDispatcher(); +$listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); // Listen for `event_name` -$dispatcher->addListener( 'event_name', function () { echo 'Event Called'; }, 10 ); +$listenerProvider->addListener( 'event_name', function () { echo 'Event Called'; }, 10 ); +$globalDispatcher = new \ItalyStrap\Event\GlobalDispatcher(); // This will echo 'Event Called' on `event_name` -$dispatcher->dispatch( 'event_name' ); +$globalDispatcher->trigger( 'event_name' ); ``` ### Simple example for filters ```php -use ItalyStrap\Event\EventDispatcher; - -$dispatcher = new EventDispatcher(); +$listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); // Listen for `event_name` -$dispatcher->addListener( 'event_name', function ( array $value ) { +$listenerProvider->addListener( 'event_name', function ( array $value ) { // $value['some-key'] === 'some-value'; true // Do your stuff here in the same ways you do with filters @@ -62,38 +224,13 @@ $dispatcher->addListener( 'event_name', function ( array $value ) { /** @var array $value */ $value = [ 'some-key' => 'some-value' ]; -// This will filters '$value' on `event_name` -$filtered_value = $dispatcher->filter( 'event_name', $value ); +$globalDispatcher = new \ItalyStrap\Event\GlobalDispatcher(); +// This will filter '$value' on `event_name` +$filtered_value = $globalDispatcher->filter( 'event_name', $value ); ``` Ok, so, for now it is very straightforward, you will use it like you use the WordPress Plugin API but more OOP oriented, -you can inject the `EventDispatcher::class` into yours classes. - -```php -use ItalyStrap\Event\EventDispatcher; -use ItalyStrap\Event\EventDispatcherInterface; - -$dispatcher = new EventDispatcher(); - -class MyClass { - - /** - * @var EventDispatcherInterface - */ - private $dispatcher; - public function __construct( EventDispatcherInterface $dispatcher ) { - $this->dispatcher = $dispatcher; - } - - public function doSomeStuffWithDispatcher() { - // Do your stuff here with hooks - // $this->dispatcher->dispatch() or $this->dispatcher->addEventListener() or $this->dispatcher->removeEventListener() - } -} - -$my_class = new MyClass( $dispatcher ); -$my_class->doSomeStuffWithDispatcher(); -``` +you can inject the `GlobalDispatcher::class` or `GlobalOrderedListenerProvider::class` into yours classes. ### The SubscriberRegister @@ -101,11 +238,12 @@ What about the Subscriber Register? Here a simple example: ```php -use ItalyStrap\Event\EventDispatcher; +use ItalyStrap\Event\GlobalDispatcher; +use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscriberInterface; -// Your class must implements the ItalyStrap\Event\SubscriberInterface +// Your class must implement the ItalyStrap\Event\SubscriberInterface class MyClassSubscriber implements SubscriberInterface { // Now add the method from the interface and return an iterable with @@ -114,21 +252,22 @@ class MyClassSubscriber implements SubscriberInterface { return ['event_name' => 'methodName']; } - public function methodName(/* could have some arguments if you use the ::filter() method */){ + public function methodName(/* could have some arguments */){ // Do some stuff with hooks } } $subscriber = new MyClassSubscriber(); -$dispatcher = new EventDispatcher(); -$subscriber_register = new SubscriberRegister( $dispatcher ); -$subscriber_register->addSubscriber( $subscriber ); +$globalDispatcher = new GlobalDispatcher(); +$listenerProvider = new GlobalOrderedListenerProvider(); +$subscriberRegister = new SubscriberRegister($listenerProvider); +$subscriberRegister->addSubscriber($subscriber); // It will execute the subscriber MyClassSubscriber::methodName -$dispatcher->dispatch( 'event_name' ); +$globalDispatcher->trigger('event_name', $some_value); // or -$dispatcher->filter( 'event_name', $some_value ); +$globalDispatcher->filter('event_name', $some_value); ``` A subscriber is a class that implements the `ItalyStrap\Event\SubscriberInterface::class` interface and could be the listener itself or a class wrapper that delegates the execution of the method on certain event @@ -288,7 +427,7 @@ In case the subscriber has a lot of events to subscribe it is better to (separat subscriber in another class and then use the subscriber to do the registration of the other class like this: ```php -use ItalyStrap\Event\EventDispatcher; +use ItalyStrap\Event\GlobalDispatcher; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscriberInterface; @@ -336,12 +475,12 @@ class MyClassSubscriber implements SubscriberInterface { $logic = new MyBusinessLogic(); $subscriber = new MyClassSubscriber( $logic ); -$dispatcher = new EventDispatcher(); +$dispatcher = new GlobalDispatcher(); $subscriber_register = new SubscriberRegister( $dispatcher ); $subscriber_register->addSubscriber( $subscriber ); // It will execute the subscriber MyClassSubscriber::methodName -$dispatcher->dispatch( 'event_name' ); +$dispatcher->trigger( 'event_name' ); // or $dispatcher->filter( 'event_name', ['some_value'] ); @@ -349,69 +488,44 @@ $dispatcher->filter( 'event_name', ['some_value'] ); $subscriber_register->removeSubscriber( $subscriber ); // The instance of the subscriber you want to remove MUST BE the same instance of the subscriber you -// added earlier and BEFORE you dispatch the event. +// added earlier, and BEFORE you dispatch the event. ``` -This library is similar to the (Symfony Event Dispatcher)[https://symfony.com/doc/current/components/event_dispatcher.html] +This library is similar to the [Symfony Event Dispatcher](https://symfony.com/doc/current/components/event_dispatcher.html) -### Example with WordPress event name -```php -// Filter the title -use ItalyStrap\Event\EventDispatcher; - -$dispatcher = new EventDispatcher(); -$dispatcher->filter( 'the_title', function ( string $title ): string { - return \mb_strtoupper( $title ); // A very dumb example -} ); - -// Execute some action -$dispatcher->dispatch( 'after_setup_theme', function (): void { - // Bootstrap your logic for theme configuration -} ); -``` +[🆙](#table-of-contents) ## Advanced Usage -If you want more power you can use the (Empress library)[https://github.com/ItalyStrap/empress] with this library +If you want more power you can use the [Empress library](https://github.com/ItalyStrap/empress) with this library The benefit is that now you can do auto-wiring for your application, lazy loading you listener/subscriber and so on. ```php use ItalyStrap\Config\ConfigFactory; -use ItalyStrap\Empress\AurynResolver; +use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\Injector; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscribersConfigExtension; -use ItalyStrap\Event\EventDispatcher; +use ItalyStrap\Event\GlobalDispatcher; +use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Event\SubscriberInterface; // From Subscriber.php class Subscriber implements SubscriberInterface { - public $check = 0; + public int $check = 0; - /** - * @var \stdClass - */ - private $stdClass; + private \stdClass $stdClass; - /** - * Subscriber constructor. - * @param \stdClass $stdClass - */ - public function __construct( \stdClass $stdClass ) { + public function __construct(\stdClass $stdClass) { $this->stdClass = $stdClass; } - /** - * @inheritDoc - */ public function getSubscribedEvents(): array { - return [ - 'event' => 'method', - ]; + yield 'event' => $this; } - public function method() { + public function __invoke() { echo 'Some text'; } } @@ -427,65 +541,67 @@ $injector = new Injector(); // Do not use it for locating services $injector->share($injector); -// Now it's time to create a configuration for dependencies to inject in the AurynResolver +// Now it's time to create a configuration for dependencies to inject in the AurynConfig $dependencies = ConfigFactory::make([ - // Share the instances of the EventDispatcher and SubscriberRegister - AurynResolver::SHARING => [ - EventDispatcher::class, + // Share the instances of the GlobalDispatcher and SubscriberRegister + AurynConfig::SHARING => [ + GlobalDispatcher::class, SubscriberRegister::class, ], - // Now add in the array all your subscribers that implemente the ItalyStrap\Event\SubscriberInterface + // Now add in the array all your subscribers that implement the ItalyStrap\Event\SubscriberInterface // The instances create are shared by default for later removing like you se above. SubscribersConfigExtension::SUBSCRIBERS => [ Subscriber::class, ], - // You can also add more configuration for the AurynResolver https://github.com/ItalyStrap/empress + // You can also add more configuration for the AurynConfig https://github.com/ItalyStrap/empress ]); -// This wil instantiate the EventResolverExtension::class -$event_resolver = $injector->make( SubscribersConfigExtension::class, [ +// This will instantiate the EventResolverExtension::class +$eventResolver = $injector->make(SubscribersConfigExtension::class, [ // In the EventResolverExtension object you can pass a config key value pair for adding or not listener at runtime // from your theme or plugin options ':config' => ConfigFactory::make([ // If the 'option_key_for_subscriber' is true than the Subscriber::class will load 'option_key_for_subscriber' => Subscriber::class // Optional ]), -] ); +]); -// Create the object for the AurynResolver::class and pass the instance of $injector and the dependencies collection -$empress = new AurynResolver( $injector, $dependencies ); +// Create the object for the AurynConfig::class and pass the instance of $injector and the dependencies collection +$empress = new \ItalyStrap\Empress\AurynConfig($injector, $dependencies); // Is the same as above if you want to use Auryn and you have shared the Auryn instance: -$empress = $injector->make( AurynResolver::class, [ +$empress = $injector->make(AurynConfig::class, [ ':dependencies' => $dependencies -] ); +]); // Pass the $event_resolver object created earlier -$empress->extend( $event_resolver ); +$empress->extend($eventResolver); // When you are ready call the resolve() method for auto-wiring your application $empress->resolve(); $this->expectOutputString( 'Some text' ); -( $injector->make( EventDispatcher::class ) )->dispatch( 'event' ); +($injector->make(GlobalDispatcher::class))->trigger('event'); // or -$dispatcher = $injector->make( EventDispatcher::class ); -$dispatcher->dispatch( 'event' ); +$dispatcher = $injector->make(GlobalDispatcher::class); +$dispatcher->trigger('event'); // $dispatcher will be the same instance because you have shared it in the above code ``` ### Lazy Loading a subscriber -To lazy load a subscriber you can simply add in the AurynResolver configuration a new value +To lazy load a subscriber you can simply add in the AurynConfig configuration a new value for proxy, see the example below: + ```php use ItalyStrap\Config\ConfigFactory; -use ItalyStrap\Empress\AurynResolver; +use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\Injector; use ItalyStrap\Event\SubscriberRegister; use ItalyStrap\Event\SubscribersConfigExtension; -use ItalyStrap\Event\EventDispatcher; +use ItalyStrap\Event\GlobalDispatcher; +use ItalyStrap\Event\GlobalOrderedListenerProvider; use ItalyStrap\Event\SubscriberInterface; // From MyBusinessLogic.php @@ -505,11 +621,9 @@ class MyBusinessLogic { } // From MyClassSubscriber.php class MyClassSubscriber implements SubscriberInterface { - /** - * @var MyBusinessLogic - */ - private $logic; - public function __construct( MyBusinessLogic $logic ) { + + private MyBusinessLogic $logic; + public function __construct(MyBusinessLogic $logic) { // This will be the proxy version of the $logic object $this->logic = $logic; } @@ -548,27 +662,27 @@ $injector = new Injector(); // Do not use it for locating services $injector->share($injector); -// Now it's time to create a configuration for dependencies to inject in the AurynResolver +// Now it's time to create a configuration for dependencies to inject in the AurynConfig $dependencies = ConfigFactory::make([ - // Share the instances of the EventDispatcher and SubscriberRegister - AurynResolver::SHARING => [ - EventDispatcher::class, + // Share the instances of the GlobalDispatcher and SubscriberRegister + AurynConfig::SHARING => [ + GlobalDispatcher::class, SubscriberRegister::class, ], // Now we declare what class we need to lazy load // In our case is the MyBusinessLogic::class injected in the MyClassSubscriber::class - AurynResolver::PROXY => [ + AurynConfig::PROXY => [ MyBusinessLogic::class, ], - // Now add in the array all your subscribers that implemente the ItalyStrap\Event\SubscriberInterface + // Now add in the array all your subscribers that implement the ItalyStrap\Event\SubscriberInterface // The instances create are shared by default for later removing like you se above. SubscribersConfigExtension::SUBSCRIBERS => [ MyClassSubscriber::class, ], - // You can also add more configuration for the AurynResolver https://github.com/ItalyStrap/empress + // You can also add more configuration for the AurynConfig https://github.com/ItalyStrap/empress ]); -// This wil instantiate the EventResolverExtension::class +// This will instantiate the EventResolverExtension::class $event_resolver = $injector->make( SubscribersConfigExtension::class, [ // In the EventResolverExtension object you can pass a config key value pair for adding or not listener at runtime // from your theme or plugin options @@ -578,11 +692,11 @@ $event_resolver = $injector->make( SubscribersConfigExtension::class, [ ]), ] ); -// Create the object for the AurynResolver::class and pass the instance of $injector and the dependencies collection -$empress = new AurynResolver( $injector, $dependencies ); +// Create the object for the AurynConfig::class and pass the instance of $injector and the dependencies collection +$empress = new AurynConfig( $injector, $dependencies ); // Is the same as above if you want to use Auryn and you have shared the Auryn instance: -$empress = $injector->make( AurynResolver::class, [ +$empress = $injector->make( AurynConfig::class, [ ':dependencies' => $dependencies ] ); @@ -592,35 +706,41 @@ $empress->extend( $event_resolver ); // When you are ready call the resolve() method for auto-wiring your application $empress->resolve(); -$dispatcher = $injector->make( EventDispatcher::class ); -$dispatcher->dispatch( 'event_name_one' ); -$dispatcher->dispatch( 'event_name_two' ); -$dispatcher->dispatch( 'event_name_three' ); +$dispatcher = $injector->make(GlobalDispatcher::class); +$dispatcher->trigger('event_name_one'); +$dispatcher->trigger('event_name_two'); +$dispatcher->trigger('event_name_three'); ``` Remember that the proxy version of an object is a "dumb" object that do nothing until you call some method, and the real object will be executed, this is useful for run code only when you need it to run. -Example with pseudo code; +Example with pseudocode; ```php \do_action('save_post', [$proxyObject, 'executeOnlyOnSavePost']); ``` -You can find more information about the (EmpressAurynResolver here)[https://github.com/ItalyStrap/empress] +You can find more information about the (Empress\AurynConfig here)[https://github.com/ItalyStrap/empress] You can find an implementation in the (ItalyStrap Theme Framework)[https://github.com/ItalyStrap/italystrap] > TODO https://inpsyde.com/en/remove-wordpress-hooks/ +[🆙](#table-of-contents) + ## Contributing All feedback / bug reports / pull requests are welcome. +[🆙](#table-of-contents) + ## License Copyright (c) 2019 Enea Overclokk, ItalyStrap This code is licensed under the [MIT](LICENSE). +[🆙](#table-of-contents) + ## Credits ### For the Event implementation diff --git a/codecept b/codecept deleted file mode 100644 index f98503f..0000000 --- a/codecept +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh - -dir=$(cd "${0%[/\\]*}" > /dev/null; cd "vendor/codeception/codeception" && pwd) - -if [ -d /proc/cygdrive ]; then - case $(which php) in - $(readlink -n /proc/cygdrive)/*) - # We are in Cygwin using Windows php, so the path must be translated - dir=$(cygpath -m "$dir"); - ;; - esac -fi - -"${dir}/codecept" "$@" diff --git a/codecept.bat b/codecept.bat deleted file mode 100644 index e3b00e2..0000000 --- a/codecept.bat +++ /dev/null @@ -1,4 +0,0 @@ -@ECHO OFF -setlocal DISABLEDELAYEDEXPANSION -SET BIN_TARGET=%~dp0vendor/codeception/codeception/codecept -php "%BIN_TARGET%" %* diff --git a/codeception.dist.yml b/codeception.dist.yml index fc82d69..af68839 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -22,3 +22,5 @@ coverage: enabled: true include: - src/* +settings: + shuffle: true diff --git a/composer.json b/composer.json index 65e88fa..570a0eb 100644 --- a/composer.json +++ b/composer.json @@ -14,36 +14,61 @@ ], "minimum-stability": "stable", "require": { - "php" : ">=7.2", + "php" : ">=7.4", "psr/log": "^1.1" }, "require-dev": { - "lucatume/wp-browser": "2.2", - "codeception/c3": "2.*", + "lucatume/wp-browser": "^3.1", "lucatume/function-mocker-le": "^1.0", - "wp-coding-standards/wpcs": "^2.1", + "codeception/module-asserts": "^1.0", + "phpspec/prophecy-phpunit": "^2.0", + + "squizlabs/php_codesniffer": "^3.7", "phpcompatibility/php-compatibility": "^9.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", - "phpstan/phpstan": "^0.11.16", - "szepeviktor/phpstan-wordpress": "^0.3.0", - "phpbench/phpbench": "@dev", - "infection/infection": "^0.15.3", - "vimeo/psalm": "^3.9", - "phpmetrics/phpmetrics": "^2.5", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + + "vimeo/psalm": "^5.6", + "humanmade/psalm-plugin-wordpress": "^3.0.0-alpha1", + + "phpbench/phpbench": "^1.2", + "phpmetrics/phpmetrics": "^2.8", + + "infection/infection": "^0.26.6", + "infection/codeception-adapter": "^0.4.1", + + "rector/rector": "^0.15.17", "psr/container": "^1.0", "psr/event-dispatcher": "^1.0", - "fig/event-dispatcher-util": "^1.0", + "fig/event-dispatcher-util": "^1.3", "crell/tukio": "^1.0", "inpsyde/object-hooks-remover": "^0.1", "italystrap/config": "^2.2", - "italystrap/debug": "~2.0", - "italystrap/empress": "~1.0" + "italystrap/debug": "dev-master", + "italystrap/empress": "dev-master", + "inpsyde/wp-stubs": "dev-main" }, "autoload": { "psr-4": { - "ItalyStrap\\Event\\": "src/Event/", - "ItalyStrap\\PsrDispatcher\\": "src/PsrDispatcher/" - } + "ItalyStrap\\Event\\": "src/", + "ItalyStrap\\PsrDispatcher\\": "tests/_data/experiment/PsrDispatcher/" + }, + "files": [ + "namespace-bc-aliases.php" + ] + }, + "autoload-dev": { + "psr-4": { + "ItalyStrap\\Tests\\": [ + "tests/src/", + "tests/_data/fixtures/src/" + ], + "ItalyStrap\\Tests\\Unit\\": "tests/unit/", + "ItalyStrap\\Tests\\Integration\\": "tests/integration/" + }, + "files": [ + "tests/_data/fixtures/classes.php", + "tests/_data/fixtures/functions.php" + ] }, "provide": { "psr/event-dispatcher-implementation": "1.0" @@ -53,33 +78,60 @@ "inpsyde/objects-hooks-remover": "Package to remove WordPress hook callbacks that uses object methods or closures." }, "scripts": { - "test": [ - "test" - ], "cs": [ - "vendor\\bin\\phpcbf -p --ignore=./tests/_support/* ./src/ ./tests/ && vendor\\bin\\phpcs -p --ignore=./tests/_support/* ./src/ ./tests/" + "@php vendor/bin/phpcs -p" + ], + "cs:fix": [ + "@php vendor/bin/phpcbf -p" ], - "analyze": [ - "vendor\\bin\\phpstan analyze --level=max && vendor\\bin\\psalm" + "psalm": [ + "@php ./vendor/bin/psalm --no-cache" ], "unit": [ - "vendor\\bin\\codecept run unit && vendor\\bin\\infection --threads=8" + "@php ./vendor/bin/codecept run unit" + ], + "integration": [ + "@php ./vendor/bin/codecept run integration" + ], + "infection": [ + "echo \"Running Infection...\"", + "echo \"Also remember to escape suite correctly, example --skip=integration or --skip=wpunit\"", + "@php ./vendor/bin/infection --threads=max" ], "bench": [ - "vendor\\bin\\phpbench run --report=performance" + "@php ./vendor/bin/phpbench run tests/Benchmark --report=aggregate" ], "metrics": [ - "vendor\\bin\\phpmetrics --report-html='./tests/_output/report' ./src" + "@php ./vendor/bin/phpmetrics --report-html='./tests/_output/report' ./src" ], "insights": [ - "vendor\\bin\\phpinsights" + "@php ./vendor/bin/phpinsights" ], "clean": [ - "vendor\\bin\\codecept clean" + "@php ./vendor/bin/codecept clean" + ], + "qa": [ + "@cs", + "@psalm", + "@unit", + "@integration", + "@infection" + ], + "rector": [ + "@php ./vendor/bin/rector process --dry-run" + ], + "rector:fix": [ + "@php ./vendor/bin/rector process" ] }, "support" : { "issues": "https://github.com/ItalyStrap/event/issues", "source": "https://github.com/ItalyStrap/event" + }, + "config": { + "allow-plugins": { + "infection/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/index.php b/index.php index 5fcaa04..b118f6c 100644 --- a/index.php +++ b/index.php @@ -29,15 +29,33 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -if ( ! \function_exists( 'd_footer' ) ) { - function d_footer( ...$args ) { - \add_action( 'shutdown', function () use ( $args ) { - d( ...$args ); - } ); - } -} - -add_action( 'plugins_loaded', function () { - require( __DIR__ . '/vendor/autoload.php' ); -// require 'example.php'; -} ); +add_action('plugins_loaded', function () { + require __DIR__ . '/vendor/autoload.php'; + + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $state = new \ItalyStrap\Event\GlobalState(); + + $event = new \stdClass(); + + $listenerProvider->addListener(\stdClass::class, function (object $event) { + $event->name = 'Hello'; + }, 10); + + $listener = new class ($state) { + private $state; + + public function __construct(\ItalyStrap\Event\GlobalState $state) { + $this->state = $state; + } + public function __invoke(object $event) { + $event->name .= ' World'; + $event->currentState = $this->state->currentEventName(); + } + }; + + $listenerProvider->addListener(\stdClass::class, $listener, 20); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider, $state); + + $name = $dispatcher->dispatch($event)->name; +}); diff --git a/infection.json.dist b/infection.json.dist index 9d8e7c1..64de405 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -4,11 +4,14 @@ "src" ] }, + "logs": { + "text": "php://stdout" + }, "timeout": 10, "mutators": { "@default": true }, - "tmpDir": "\/tests\/_output", + "tmpDir": "/tests/_output", "testFramework": "codeception", - "testFrameworkOptions": "--skip=wpunit --skip=acceptance --skip=functional" + "testFrameworkOptions": "--skip=integration" } \ No newline at end of file diff --git a/namespace-bc-aliases.php b/namespace-bc-aliases.php new file mode 100644 index 0000000..7be384e --- /dev/null +++ b/namespace-bc-aliases.php @@ -0,0 +1,13 @@ + - - - A custom set of rules to check for a WPized WordPress project + + A custom set of rules to check for this project @@ -10,38 +13,14 @@ - - - - - - - - - - - - + - - + ./src/ + ./tests/ - - - - - - - - - - - - - + - - - + */vendor/* + */tests/_support/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 0f15b7e..0000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,22 +0,0 @@ -#$ composer update --optimize-autoloader -#$ vendor/bin/phpstan analyze - -includes: - # @see https://github.com/phpstan/phpstan/blob/master/conf/bleedingEdge.neon - - vendor/phpstan/phpstan/conf/bleedingEdge.neon - - vendor/szepeviktor/phpstan-wordpress/extension.neon -parameters: - level: max - inferPrivatePropertyTypeFromConstructor: true - paths: - - %currentWorkingDirectory%/src/ - excludes_analyse: - - %currentWorkingDirectory%/src/Config/ - autoload_files: - # Procedural code - #- %currentWorkingDirectory%/functions/autoload.php - ignoreErrors: - # Uses func_get_args() - #- '#^Function apply_filters invoked with [34567] parameters, 2 required\.$#' - #- '#Access to an undefined property ItalyStrap\\Config\\Config::\$[a-zA-Z0-9_]+#' - #- '#Access to an undefined property ItalyStrap\\Config\\ConfigInterface::\$[a-zA-Z0-9_]+#' diff --git a/psalm.xml b/psalm.xml index 1e6be44..8fff84f 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,11 +1,13 @@ @@ -26,4 +28,12 @@ - + + + + + + + + + \ No newline at end of file diff --git a/src/DebugDispatcher.php b/src/DebugDispatcher.php new file mode 100644 index 0000000..9604de3 --- /dev/null +++ b/src/DebugDispatcher.php @@ -0,0 +1,35 @@ +dispatcher = $dispatcher; + $this->logger = $logger; + } + + public function dispatch(object $event): object + { + $this->logger->debug(self::M_DEBUG, ['type' => get_class($event), 'event' => $event]); + return $this->dispatcher->dispatch($event); + } +} diff --git a/src/Dispatcher.php b/src/Dispatcher.php new file mode 100644 index 0000000..2cfd9d0 --- /dev/null +++ b/src/Dispatcher.php @@ -0,0 +1,47 @@ +listenerProvider = $listenerProvider; + $this->state = $state ?? new NullState(); + } + + public function dispatch(object $event): object + { + $this->state->forEvent($event, $this); + $this->state->progress(StateInterface::BEFORE, $this); + + /** @var callable $listener */ + foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) { + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + break; + } + + $listener($event); + } + + $this->state->progress(StateInterface::AFTER, $this); + + return $event; + } +} diff --git a/src/Event/EventDispatcher.php b/src/Event/EventDispatcher.php deleted file mode 100644 index ca391bc..0000000 --- a/src/Event/EventDispatcher.php +++ /dev/null @@ -1,89 +0,0 @@ -execute( ...func_get_args() ); - } - - /** - * @inheritDoc - */ - public function filter( string $event_name, $value, ...$args ) { - return apply_filters( ...func_get_args() ); - } - - /** - * @inheritDoc - */ - public function currentEventName() { - return current_filter(); - } - - /** - * @inheritDoc - */ - public function hasListener( string $event_name, $function_to_check = false ) { - return has_filter( $event_name, $function_to_check ); - } -} diff --git a/src/Event/EventDispatcherInterface.php b/src/Event/EventDispatcherInterface.php deleted file mode 100644 index 4fb46c8..0000000 --- a/src/Event/EventDispatcherInterface.php +++ /dev/null @@ -1,108 +0,0 @@ - 'method_name'] - * * [ - * 'event_name' => - * [ - * KEYS::CALLBACK => 'method_name', - * KEYS::PRIORITY => $priority, - * ] - * ] - * * [ - * 'event_name' => - * [ - * KEYS::CALLBACK => 'method_name', - * KEYS::PRIORITY => $priority, - * KEYS::ACCEPTED_ARGS => $accepted_args, - * ] - * ] - * - * @return iterable - */ - public function getSubscribedEvents(): iterable; -} diff --git a/src/Event/SubscriberRegister.php b/src/Event/SubscriberRegister.php deleted file mode 100644 index b0b4f0c..0000000 --- a/src/Event/SubscriberRegister.php +++ /dev/null @@ -1,129 +0,0 @@ -dispatcher = $dispatcher; - } - - /** - * @inheritDoc - */ - public function addSubscriber( Subscriber $subscriber ): void { - foreach ( $subscriber->getSubscribedEvents() as $event_name => $parameters ) { - if ( isset( $parameters[0] ) && is_iterable( $parameters[0] ) ) { - foreach ( $parameters as $listener ) { - $this->addSubscriberListener( $subscriber, $event_name, $listener ); - } - continue; - } - - $this->addSubscriberListener( $subscriber, $event_name, $parameters ); - } - } - - /** - * Adds the given subscriber listener to the list of event listeners - * that listen to the given event. - * - * @param Subscriber $subscriber - * @param string $event_name - * @param string|array $parameters - */ - private function addSubscriberListener( Subscriber $subscriber, string $event_name, $parameters ): void { - $this->dispatcher->addListener( - $event_name, - $this->buildCallable( $subscriber, $parameters ), - ...$this->buildParameters( $parameters ) - ); - } - - /** - * @inheritDoc - */ - public function removeSubscriber( Subscriber $subscriber ): void { - foreach ( $subscriber->getSubscribedEvents() as $event_name => $parameters ) { - if ( isset( $parameters[0] ) && is_iterable( $parameters[0] ) ) { - foreach ( $parameters as $listener ) { - $this->removeSubscriberListener( $subscriber, $event_name, $listener ); - } - continue; - } - $this->removeSubscriberListener( $subscriber, $event_name, $parameters ); - } - } - - /** - * Adds the given subscriber listener to the list of event listeners - * that listen to the given event. - * - * @param Subscriber $subscriber - * @param string $event_name - * @param string|array $parameters - */ - private function removeSubscriberListener( Subscriber $subscriber, string $event_name, $parameters ): void { - $this->dispatcher->removeListener( - $event_name, - $this->buildCallable( $subscriber, $parameters ), - ...$this->buildParameters( $parameters ) - ); - } - - /** - * @param Subscriber $subscriber - * @param string|array $parameters - * @return callable - */ - private function buildCallable( Subscriber $subscriber, $parameters ): callable { - $callable = null; - - if ( is_string( $parameters ) ) { - /** @var callable $callable */ - $callable = [$subscriber, $parameters]; - } elseif ( isset( $parameters[ Subscriber::CALLBACK ] ) ) { - /** @var callable $callable */ - $callable = [$subscriber, $parameters[ Subscriber::CALLBACK ]]; - } else { - throw new RuntimeException( sprintf( - 'Impossible to build a valid callable because $parameters is a type %s', - gettype( $parameters ) - )); - } - - return $callable; - } - - /** - * @param mixed $parameters - * @return array - */ - private function buildParameters( $parameters ): array { - return [ - $parameters[ Subscriber::PRIORITY ] ?? self::PRIORITY, - $parameters[ Subscriber::ACCEPTED_ARGS ] ?? self::ACCEPTED_ARGS, - ]; - } -} diff --git a/src/Event/SubscriberRegisterInterface.php b/src/Event/SubscriberRegisterInterface.php deleted file mode 100644 index 93a8b59..0000000 --- a/src/Event/SubscriberRegisterInterface.php +++ /dev/null @@ -1,32 +0,0 @@ -event_manager = $event_manager; - $this->config = $config; - } - - /** - * @inheritDoc - */ - public function name(): string { - return self::SUBSCRIBERS; - } - - /** - * @inheritDoc - */ - public function execute( AurynConfigInterface $application ): void { - $application->walk( $this->name(), [$this, 'walk'] ); - } - - /** - * @param string $class Array value from yous configuration - * @param int|string $index_or_optionName Array key from your configuration - * @param Injector $injector An instance of the Injector::class - * @throws ConfigException - * @throws InjectionException - */ - public function walk( string $class, $index_or_optionName, Injector $injector ): void { - - if ( \is_string( $index_or_optionName ) && empty( $this->config->get( $index_or_optionName, false ) ) ) { - return; - } - - $this->event_manager->addSubscriber( $injector->share( $class )->make( $class ) ); - } -} diff --git a/src/EventSubscription.php b/src/EventSubscription.php new file mode 100644 index 0000000..685cc3e --- /dev/null +++ b/src/EventSubscription.php @@ -0,0 +1,40 @@ +eventSubscriber = [ + SubscriberInterface::CALLBACK => $callback, + SubscriberInterface::PRIORITY => $priority, + SubscriberInterface::ACCEPTED_ARGS => $acceptedArgs, + ]; + } + + public function toArray(): array + { + return $this->eventSubscriber; + } + + public function __invoke(): array + { + return $this->eventSubscriber; + } +} diff --git a/src/GlobalDispatcher.php b/src/GlobalDispatcher.php new file mode 100644 index 0000000..9922b34 --- /dev/null +++ b/src/GlobalDispatcher.php @@ -0,0 +1,26 @@ + [ // $priority + * 'callback1' => [ + * 'function' => 'callback1', // 'callback1' is the name of the function or $idx + * 'accepted_args' => 1, + * ], + * 'callback2' => [ + * 'function' => 'callback2', + * 'accepted_args' => 1, + * ], + * ], + * ] + * @var array> $callbacks + */ + foreach ($wp_filter[$eventName]->callbacks as $callbacks) { + foreach ($callbacks as $callback) { + yield $callback['function']; + } + } + } +} diff --git a/src/GlobalState.php b/src/GlobalState.php new file mode 100644 index 0000000..032e0d7 --- /dev/null +++ b/src/GlobalState.php @@ -0,0 +1,83 @@ +eventName = \get_class($event); + global $wp_current_filter; + $wp_current_filter[] = $this->eventName; + } + + public function progress(string $state, \Psr\EventDispatcher\EventDispatcherInterface $provider): void + { + switch ($state) { + case self::BEFORE: + $this->incrDispatchedEvent(); + break; + case self::AFTER: + $this->dispatchedEvent(); + break; + default: + throw new \InvalidArgumentException(\sprintf( + 'The state "%s" is not valid', + $state + )); + } + } + + private function incrDispatchedEvent(): void + { + global $wp_actions, $wp_filters; + /** @var array $wp_actions*/ + $wp_actions[$this->eventName] = ($wp_actions[$this->eventName] ?? 0) + 1; + /** @var array $wp_filters*/ + $wp_filters[$this->eventName] = ($wp_filters[$this->eventName] ?? 0) + 1; + } + + private function dispatchedEvent(): void + { + global $wp_current_filter; + \array_pop($wp_current_filter); + } + + /** + * Suppressing this error because if for any reason the global $wp_current_filter is empty + * this method will return false instead of an empty string. + * @psalm-suppress RedundantCastGivenDocblockType + */ + public function currentEventName(): string + { + return (string)\current_filter(); + } + + public function dispatchedEventCount(): int + { + // In this case I call did_action() instead of did_filter() because in unit test the function was not defined. + return \did_action($this->eventName); + } + + public function isDispatching(): bool + { + return \doing_filter($this->eventName); + } + + public function __debugInfo() + { + return [ + 'eventName' => $this->eventName, + 'currentEvent' => $this->currentEventName(), + 'dispatchingEvent' => $this->isDispatching(), + 'dispatchedEventCount' => $this->dispatchedEventCount(), + ]; + } +} diff --git a/src/LegacyEventDispatcherMethodsDeprecatedTrait.php b/src/LegacyEventDispatcherMethodsDeprecatedTrait.php new file mode 100644 index 0000000..e241c5f --- /dev/null +++ b/src/LegacyEventDispatcherMethodsDeprecatedTrait.php @@ -0,0 +1,152 @@ +deprecated( + $this->deprecationPattern, + __FUNCTION__, + '\ItalyStrap\Event\GlobalOrderedListenerProvider::' . __FUNCTION__ + ); + + return add_filter($eventName, $listener, $priority, $accepted_args); + } + + public function removeListener( + string $eventName, + callable $listener, + int $priority = 10 + ): bool { + + $this->deprecated( + $this->deprecationPattern, + __FUNCTION__, + '\ItalyStrap\Event\GlobalOrderedListenerProvider::' . __FUNCTION__ + ); + + return remove_filter($eventName, $listener, $priority); + } + + /** + * @param string $eventName + * @param int|false $priority + * @return bool + */ + public function removeAllListener(string $eventName, $priority = false): bool + { + + $this->deprecated( + $this->deprecationPattern, + __FUNCTION__, + '\ItalyStrap\Event\GlobalOrderedListenerProvider::' . __FUNCTION__ + ); + + return remove_all_filters($eventName, $priority); + } + + /** + * @param string $eventName + * @param array|callable|false|string $callback + * @return bool|int + */ + public function hasListener(string $eventName, $callback = false) + { + + $this->deprecated( + $this->deprecationPattern, + __FUNCTION__, + '\ItalyStrap\Event\GlobalOrderedListenerProvider::' . __FUNCTION__ + ); + + return has_filter($eventName, $callback); + } + + /** + * @param string $event_name + * @param mixed ...$args + * @return object + * @infection-ignore-all + * @psalm-suppress PossiblyUnusedParam + */ + public function dispatch($event_name, ...$args): object + { + + $this->deprecated( + $this->deprecationPattern, + __FUNCTION__, + '\Psr\EventDispatcher\EventDispatcherInterface::' . __FUNCTION__ + ); + + $this->trigger($event_name, ...$args); + + if (isset($args[0]) && \is_object($args[0])) { + return $args[0]; + } + + return new class { + }; + } + + /** + * @deprecated + * @infection-ignore-all + * @param mixed ...$args + * @psalm-suppress PossiblyUnusedParam + */ + public function execute(string $event_name, ...$args): void + { + $pattern = <<<'D_MESSAGE' +This method %1$s::%2$s() is deprecated, use %1$s::%3$s() instead. +D_MESSAGE; + + $this->deprecated($pattern, __FUNCTION__, 'trigger'); + + $this->trigger($event_name, ...$args); + } + + public function currentEventName(): string + { + + $this->deprecated( + $this->deprecationPattern, + __FUNCTION__, + '\ItalyStrap\Event\GlobalState::' . __FUNCTION__ + ); + + return current_filter(); + } + + /** + * @psalm-suppress UnusedMethod + */ + private function deprecated(string $pattern, string $oldMethodName, string $newMethodName): void + { + \trigger_error( + \sprintf( + $pattern, + self::class, + $oldMethodName, + $newMethodName + ), + \E_USER_DEPRECATED + ); + } +} diff --git a/src/ListenerRegisterInterface.php b/src/ListenerRegisterInterface.php new file mode 100644 index 0000000..60788be --- /dev/null +++ b/src/ListenerRegisterInterface.php @@ -0,0 +1,70 @@ + [ + // Global + GlobalDispatcherInterface::class => GlobalDispatcher::class, + SubscriberRegisterInterface::class => SubscriberRegister::class, + // PSR-14 + \Psr\EventDispatcher\EventDispatcherInterface::class => Dispatcher::class, + ListenerProviderInterface::class => GlobalOrderedListenerProvider::class, + ListenerRegisterInterface::class => GlobalOrderedListenerProvider::class, + StateInterface::class => GlobalState::class, + ], + 'sharing' => [ + // Global + GlobalDispatcher::class, + SubscriberRegister::class, + // PSR-14 + Dispatcher::class, + GlobalOrderedListenerProvider::class, + GlobalState::class, + ], + ]; + } +} diff --git a/src/NullState.php b/src/NullState.php new file mode 100644 index 0000000..1644f1e --- /dev/null +++ b/src/NullState.php @@ -0,0 +1,31 @@ +stopPropagation; + } + + public function stopPropagation(): void + { + $this->stopPropagation = true; + } +} diff --git a/src/PsrDispatcher/CallableFactory.php b/src/PsrDispatcher/CallableFactory.php deleted file mode 100644 index d4fb3cd..0000000 --- a/src/PsrDispatcher/CallableFactory.php +++ /dev/null @@ -1,14 +0,0 @@ -listener = $listener; - } - - /** - * @inheritDoc - */ - public function listener(): callable { - return $this->listener; - } - - /** - * @inheritDoc - */ - public function nullListener(): void { - $this->listener = function ( object $event ): void { - }; - } - - /** - * The method called from the WordPress Plugin API on event - * This method MUST check if the $event is stoppable or not and - * then call the $listener and pass the $event object in it - * - * @param object $event - * @return void - */ - public function __invoke( object $event ) { - - if ( $event instanceof StoppableEventInterface && $event->isPropagationStopped() ) { - return; - } - - $listener = $this->listener; - $listener( $event ); - } -} diff --git a/src/PsrDispatcher/DebugDispatcher.php b/src/PsrDispatcher/DebugDispatcher.php deleted file mode 100644 index aa80fd4..0000000 --- a/src/PsrDispatcher/DebugDispatcher.php +++ /dev/null @@ -1,44 +0,0 @@ -dispatcher = $dispatcher; - $this->logger = $logger; - } - - public function dispatch(object $event) { - $this->logger->debug(self::M_DEBUG, ['type' => get_class($event), 'event' => $event]); - return $this->dispatcher->dispatch($event); - } -} diff --git a/src/PsrDispatcher/ListenerHolderInterface.php b/src/PsrDispatcher/ListenerHolderInterface.php deleted file mode 100644 index 0688dc7..0000000 --- a/src/PsrDispatcher/ListenerHolderInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -wp_filter = &$wp_filter; - $this->factory = $factory; - $this->dispatcher = $dispatcher; - } - - /** - * @inheritDoc - */ - public function addListener( - string $event_name, - callable $listener, - int $priority = 10, - int $accepted_args = 1 - ): bool { - /** @var callable $callback */ - $callback = $this->factory->buildCallable( $listener ); - return $this->dispatcher->addListener( $event_name, $callback, $priority, $accepted_args ); - } - - /** - * @inheritDoc - */ - public function removeListener( - string $event_name, - callable $listener, - int $priority = 10 - ): bool { - - if ( ! isset( $this->wp_filter[ $event_name ][ $priority ] ) ) { - return false; - } - - foreach ( (array) $this->wp_filter[ $event_name ][ $priority ] as $method_name_registered => $value ) { - if ( ! $value['function'] instanceof ListenerHolderInterface ) { - throw new \RuntimeException( \sprintf( - 'The callable is not an instance of %s', - ListenerHolderInterface::class - ) ); - } - - if ( $value['function']->listener() === $listener ) { - $value['function']->nullListener(); - } - } - - return true; - } - - /** - * @inheritDoc - */ - public function dispatch( object $event ) { - $this->dispatcher->dispatch( \get_class( $event ), $event ); - return $event; - } -} diff --git a/src/StateInterface.php b/src/StateInterface.php new file mode 100644 index 0000000..31a211c --- /dev/null +++ b/src/StateInterface.php @@ -0,0 +1,24 @@ +> + */ + public function getSubscribedEvents(): iterable; +} diff --git a/src/SubscriberRegister.php b/src/SubscriberRegister.php new file mode 100644 index 0000000..079bc3d --- /dev/null +++ b/src/SubscriberRegister.php @@ -0,0 +1,147 @@ +listenerRegister = $listenerRegister; + } + + public function addSubscriber(Subscriber $subscriber): void + { + foreach ($subscriber->getSubscribedEvents() as $event_name => $parameters) { + if (is_array($parameters) && isset($parameters[0]) && is_iterable($parameters[0])) { + foreach ($parameters as $listener) { + $this->addSubscriberListener($subscriber, $event_name, $listener); + } + continue; + } + + $this->addSubscriberListener($subscriber, $event_name, $parameters); + } + } + + /** + * Adds the given subscriber listener to the list of event listeners + * that listen to the given event. + * + * @param Subscriber $subscriber + * @param string $event_name + * @param mixed $parameters + */ + private function addSubscriberListener(Subscriber $subscriber, string $event_name, $parameters): void + { + $this->listenerRegister->addListener( + $event_name, + $this->buildCallable($subscriber, $parameters), + ...$this->buildParameters($parameters) + ); + } + + public function removeSubscriber(Subscriber $subscriber): void + { + foreach ($subscriber->getSubscribedEvents() as $event_name => $parameters) { + if (is_array($parameters) && isset($parameters[0]) && is_iterable($parameters[0])) { + foreach ($parameters as $listener) { + $this->removeSubscriberListener($subscriber, $event_name, $listener); + } + continue; + } + $this->removeSubscriberListener($subscriber, $event_name, $parameters); + } + } + + /** + * Adds the given subscriber listener to the list of event listeners + * that listen to the given event. + * + * @param Subscriber $subscriber + * @param string $event_name + * @param mixed $parameters + */ + private function removeSubscriberListener(Subscriber $subscriber, string $event_name, $parameters): void + { + $this->listenerRegister->removeListener( + $event_name, + $this->buildCallable($subscriber, $parameters), + ...$this->buildParameters($parameters) + ); + } + + /** + * @param Subscriber $subscriber + * @param mixed $parameters + * @return callable + */ + private function buildCallable(Subscriber $subscriber, $parameters): callable + { + if (is_callable($parameters)) { + return $parameters; + } + + if (is_string($parameters) && method_exists($subscriber, $parameters)) { + return [$subscriber, $parameters]; + } + + if ( + isset($parameters[Subscriber::CALLBACK]) + && is_callable($parameters[Subscriber::CALLBACK]) + ) { + return $parameters[Subscriber::CALLBACK]; + } + + if ( + isset($parameters[Subscriber::CALLBACK]) + && method_exists($subscriber, (string)$parameters[Subscriber::CALLBACK]) + ) { + return [$subscriber, $parameters[Subscriber::CALLBACK]]; + } + + throw new RuntimeException(sprintf( + 'Impossible to build a valid callable because $parameters is a type of %s', + gettype($parameters) + )); + } + + /** + * @param mixed $parameters + * @return array + */ + private function buildParameters($parameters): array + { + if (is_callable($parameters) || !is_array($parameters)) { + return [ + self::PRIORITY, + self::ACCEPTED_ARGS, + ]; + } + + return [ + (int)($parameters[Subscriber::PRIORITY] ?? self::PRIORITY), + (int)($parameters[Subscriber::ACCEPTED_ARGS] ?? self::ACCEPTED_ARGS), + ]; + } +} diff --git a/src/SubscriberRegisterInterface.php b/src/SubscriberRegisterInterface.php new file mode 100644 index 0000000..c951ced --- /dev/null +++ b/src/SubscriberRegisterInterface.php @@ -0,0 +1,33 @@ +subscriberRegister = $subscriberRegister; + $this->config = $config; + } + + /** + * @inheritDoc + */ + public function name(): string + { + return self::SUBSCRIBERS; + } + + /** + * @inheritDoc + */ + public function execute(AurynConfigInterface $application): void + { + $application->walk($this->name(), $this); + } + + /** + * @param string $class Array value from yous configuration + * @param int|string $index_or_optionName Array key from your configuration + * @param Injector $injector An instance of the Injector::class + * @throws ConfigException + * @throws InjectionException + */ + public function __invoke(string $class, $index_or_optionName, Injector $injector): void + { + + if (\is_string($index_or_optionName) && empty($this->config->get($index_or_optionName, false))) { + return; + } + + /** @var SubscriberInterface $subscriber */ + $subscriber = $injector->share($class)->make($class); + $this->subscriberRegister->addSubscriber($subscriber); + } +} diff --git a/test.bat b/test.bat deleted file mode 100644 index 002d186..0000000 --- a/test.bat +++ /dev/null @@ -1,2 +0,0 @@ -@ECHO OFF -vendor\bin\phpcbf --ignore=./tests/_support/* ./src/ ./tests/ && vendor\bin\phpcs --ignore=./tests/_support/* ./src/ ./tests/ && vendor\bin\phpstan analyze && codecept run unit && codecept run wpunit diff --git a/tests/Benchmark/DispatcherBench.php b/tests/Benchmark/DispatcherBench.php new file mode 100644 index 0000000..a8032e1 --- /dev/null +++ b/tests/Benchmark/DispatcherBench.php @@ -0,0 +1,54 @@ +dispatcherWithNullListener = new Dispatcher(new class implements ListenerProviderInterface { + public function getListenersForEvent(object $event): iterable + { + return []; + } + }); + $orderedListenerProvider = new OrderedListenerProvider(); + $orderedListenerProvider->addListener(function (\stdClass $event) { + $event->value = 'Value printed'; + return $event; + }); + $this->dispatcherWithOrderedListener = new Dispatcher($orderedListenerProvider); + $this->event = new \stdClass(); + } + + /** + * @revs (10000) + * @iterations (5) + */ + public function benchDispatchWithNullListeners(): void + { + $this->dispatcherWithNullListener->dispatch($this->event); + } + + /** + * @revs (10000) + * @iterations (5) + */ + public function benchDispatchWithOrderedListeners(): void + { + $this->dispatcherWithOrderedListener->dispatch($this->event); + } +} diff --git a/tests/Benchmark/GenericBench.php b/tests/Benchmark/GenericBench.php new file mode 100644 index 0000000..885c441 --- /dev/null +++ b/tests/Benchmark/GenericBench.php @@ -0,0 +1,42 @@ + 'Value printed'); + } + + /** + * @revs (10000) + * @iterations (5) + */ + public function benchCallUserFuncArray(): void + { + \call_user_func_array(fn() => 'Value printed', []); + } + + /** + * @revs (10000) + * @iterations (5) + */ + public function benchCallUserFuncArrayWithFiveArgs(): void + { + \call_user_func_array(fn($arg1, $arg2, $arg3, $arg4, $arg5) => 'Value printed', [1, 2, 3, 4, 5]); + } +} diff --git a/tests/TODO.md b/tests/TODO.md new file mode 100644 index 0000000..e4a5e05 --- /dev/null +++ b/tests/TODO.md @@ -0,0 +1,43 @@ +# Da aggiungere al readme quando pronto + +## Alcune basi del sistema di eventi (hooks) di WordPress + +`do_action` è essenzialmente un wrapper per `apply_filters` con l'aggiunta di logica per gestire alcune variabili globali. + +Aggiungere o rimuovere listener è possibile tramite `add_filter` e `remove_filter` che sono wrapper per `add_action` e `remove_action`, queste non aggiungono nulla e possono essere interscambiabili. +Il provider infatti utilizza `add_filter` e `remove_filter` per aggiungere e rimuovere i listener, i listener verranno poi aggiunti a `$wp_filter` che è la variabile globale usata per collezionare i listener. + +## Introduzione + +Alcune considerazioni prima d'iniziare, lo standard PSR-14 non è molto complesso, anzi, possiamo dire che sia piuttosto facile come concetto, anche il sistema utilizzato da WordPress alla fine non è molto complesso ma l'integrazione è stata comunque non semplice al primo tentativo. +Le API di WordPress in pratica sono dei wrapper per le, purtroppo, variabili globali che si occupano di gestire gli eventi e i listener, dopo una serie di tentativi senza riuscire a integrare le API nel modo giusto ho deciso di andare per la via dell'hardcoding implementando direttamente le globali usate nelle API di WordPress. +So che teoricamente sarebbe stato meglio l'utilizzo delle API perché la logica wrappata potrebbe cambiare un giorno ma, anche no, conoscendo WordPress questo non succederà mai, lol, quindi ho deciso di andare per la via più semplice e veloce. + +Il Dispatcher è il componente che si occupa di gestire gli eventi e di notificare i listener registrati a essi. +Per iniziare la migrazione allo standard PSR-14, è stato creato un nuovo dispatcher che implementa l'interfaccia Psr\EventDispatcher\EventDispatcherInterface. +Il dispatcher precedente (EventDispatcher) è stato deprecato e verrà rimosso in una versione futura di questo pacchetto. + +Il nuovo dispatcher è stato creato con lo scopo di eseguire solo il dispatch degli eventi lasciando al provider la gestione della registrazione dei listener. +Per questo motivo, il nuovo dispatcher non implementa l'interfaccia Psr\EventDispatcher\ListenerProviderInterface ma è possibile iniettare una istanza di Psr\EventDispatcher\ListenerProviderInterface nel dispatcher tramite il costruttore. +Tutto questo è stato fatto per separare la gestione degli eventi dalla gestione dei listener. + +Il dispatcher utilizza la logica presente nel core di WordPress per la gestione degli eventi e dei listener, in pratica usa le stesse globali usate da `do_action` per popolare `$wp_current_filter` e `$wp_filters` e `$wp_actions` per consentire l'utilizzo delle funzioni `current_filter()`, `has_filter()` e `did_action()`. + +Questo pacchetto infatti è pensato per poter integrare lo standard PSR-14 in WordPress. + +Il provider è un aggregatore di listener e deve essere integrato una volta istanziato nel dispatcher per poter essere utilizzato. +Il provider usa la logica presente nel core di WordPress per la gestione dei listener, quindi l'aggiunta e la rimozione dei listener è uguale alle funzioni di WordPress `add_filter()` e `remove_filter()` l'unica eccezione è che i metodi per aggiungere e rimuovere i listenere non hanno il numero di argomenti come parametro visto che lo standard PSR-14 non prevede la possibilità di passare argomenti ai listener dato che esiste un unico argomento, l'Evento. +Per poter poi creare un iterator da passare al dispatcher, il provider utilizza la variabile globale `$wp_filter` utilizzata dal core per collezionare i vari listener. + +Essendo compatibile con lo standard PSR-14, è possibile ovviamente utilizzare librerie terze che implementano lo standard PSR-14, piccola nota ovviamente questa è l'unica libreria che consente l'utilizzo anche delle API di WordPress per la gestione degli eventi. + +Il provider si chiama `OrderedListenerProvider` perché di default le API di WordPress ritornano una lista preordinata di listener in base alla priorità, che è comunque possibile indicare nei metodi `addListener()` e `removeListener()` allo stesso modo in cui viene fatto con le funzioni di WordPress `add_filter()` e `remove_filter()`. + +======= + +Non so ancora se GlobalState è la scelta migliore perché la API `current_filter()`, `doing_filter` and `did_action()` sono funzioni dove, la prima funzione ritorna il nome dell'evento corrente e la stessa cosa si può ottenere con `\get_class($event)`, le altre due funzioni prendono come parametro il nome dell'evento che è sempre possibile ottenere con `get_class($event)`. +Ora `StateInterface` serve principalmente per avere codice più OOP oriented, ad ogni modo ulteriori test sono necessari per capire se è la scelta migliore. + +## Migrazione + + diff --git a/tests/_data/experiment/PsrDispatcher/CallableFactory.php b/tests/_data/experiment/PsrDispatcher/CallableFactory.php new file mode 100644 index 0000000..4655b68 --- /dev/null +++ b/tests/_data/experiment/PsrDispatcher/CallableFactory.php @@ -0,0 +1,16 @@ +listener = $listener; + } + + /** + * @inheritDoc + */ + public function listener(): callable + { + return $this->listener; + } + + /** + * @inheritDoc + */ + public function nullListener(): void + { + $this->listener = function (object $event): void { + }; + } + + /** + * The method called from the WordPress Plugin API on event + * This method MUST check if the $event is stoppable or not and + * then call the $listener and pass the $event object in it + * + * @param object $event + * @return void + */ + public function __invoke(object $event) + { + + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + return; + } + + $listener = $this->listener; + $listener($event); + } +} diff --git a/tests/_data/experiment/PsrDispatcher/ListenerHolderInterface.php b/tests/_data/experiment/PsrDispatcher/ListenerHolderInterface.php new file mode 100644 index 0000000..d054166 --- /dev/null +++ b/tests/_data/experiment/PsrDispatcher/ListenerHolderInterface.php @@ -0,0 +1,23 @@ +wp_filter = &$wp_filter; + $this->factory = $factory; + $this->dispatcher = $dispatcher; + $this->listenerRegister = $listenerRegister; + } + + /** + * @inheritDoc + */ + public function addListener( + string $event_name, + callable $listener, + int $priority = 10, + int $accepted_args = 1 + ): bool { + /** @var callable $callback */ + $callback = $this->factory->buildCallable($listener); + return $this->listenerRegister->addListener($event_name, $callback, $priority, $accepted_args); + } + + /** + * @inheritDoc + */ + public function removeListener( + string $event_name, + callable $listener, + int $priority = 10 + ): bool { + + if (! isset($this->wp_filter[ $event_name ][ $priority ])) { + return false; + } + + foreach ((array) $this->wp_filter[ $event_name ][ $priority ] as $method_name_registered => $value) { + if (! $value['function'] instanceof ListenerHolderInterface) { + throw new \RuntimeException(\sprintf( + 'The callable is not an instance of %s', + ListenerHolderInterface::class + )); + } + + if ($value['function']->listener() === $listener) { + $value['function']->nullListener(); + } + } + + return true; + } + + /** + * @inheritDoc + */ + public function dispatch(object $event): object + { + $this->dispatcher->trigger(\get_class($event), $event); + return $event; + } +} diff --git a/tests/_data/fixtures/classes.php b/tests/_data/fixtures/classes.php index 5db07c1..4ea4893 100644 --- a/tests/_data/fixtures/classes.php +++ b/tests/_data/fixtures/classes.php @@ -8,7 +8,7 @@ use ItalyStrap\Event\SubscriberInterface; class SomeCLass { - private $state = 0; + private int $state = 0; public function doSomething() { $this->state++; return 'Test returned from: ' . __METHOD__ . ' with value: ' . $this->state; @@ -19,10 +19,7 @@ class Subscriber implements SubscriberInterface { public $check = 0; - /** - * @var \stdClass - */ - private $stdClass; + private \stdClass $stdClass; /** * Subscriber constructor. @@ -46,21 +43,18 @@ public function method() { } } -class SubscriberServiceProvider extends Subscriber implements SubscriberInterface { - /** - * @var Subscriber - */ - private $subscriber; +class SubscriberServiceProvider extends SubscriberMock implements SubscriberInterface { + private \ItalyStrap\Tests\SubscriberMock $subscriber; - public function getSubscriberObj(): Subscriber { + public function getSubscriberObj(): SubscriberMock { return $this->subscriber; } /** * constructor. - * @param Subscriber $subscriber + * @param SubscriberMock $subscriber */ - public function __construct( Subscriber $subscriber ) { + public function __construct(SubscriberMock $subscriber ) { $this->subscriber = $subscriber; } @@ -76,15 +70,15 @@ public function method() { } } -class SubscriberServiceProviderCallable extends Subscriber implements SubscriberInterface { +class SubscriberServiceProviderCallable extends SubscriberMock implements SubscriberInterface { /** - * @var Subscriber + * @var SubscriberMock */ private $subscriber; /** * constructor. - * @param Subscriber $subscriber + * @param SubscriberMock $subscriber */ public function __construct( callable $subscriber ) { $this->subscriber = $subscriber; @@ -111,12 +105,9 @@ class Listener { class ClassWithDispatchDependency { - const EVENT_NAME = 'event_name'; + public const EVENT_NAME = 'event_name'; - /** - * @var EventDispatcherInterface - */ - private $dispatcher; + private \ItalyStrap\Event\EventDispatcherInterface $dispatcher; private $value = ''; diff --git a/tests/_data/fixtures/functions.php b/tests/_data/fixtures/functions.php new file mode 100644 index 0000000..ccfa48d --- /dev/null +++ b/tests/_data/fixtures/functions.php @@ -0,0 +1,26 @@ +value = 42; +} + +function listener_change_value_to_false_and_stop_propagation( object $event ): void { + $event->value = false; + \method_exists($event, 'stopPropagation') and $event->stopPropagation(); +} + +function listener_change_value_to_77( object $event ): void { + $event->value = 77; +} + +function get_text() { + return 'new value'; +} + +function on_callback(...$args) { + +} diff --git a/tests/_data/fixtures/psr-14.php b/tests/_data/fixtures/psr-14.php deleted file mode 100644 index b58d11c..0000000 --- a/tests/_data/fixtures/psr-14.php +++ /dev/null @@ -1,48 +0,0 @@ -propagationStopped = true; - } - - /** - * @inheritDoc - */ - public function isPropagationStopped(): bool { - return $this->propagationStopped; - } -}; - -function listener_change_value_to_42( object $event ): void { - $event->value = 42; -} - -function listener_change_value_to_false_and_stop_propagation( object $event ): void { - $event->value = false; - $event->stopPropagation(); -} - -function listener_change_value_to_77( object $event ): void { - $event->value = 77; -} - -class ListenerChangeValueToText { - public function changeText( object $event ): void { - $event->value = get_text(); - } -} - -function get_text() { - return 'new value'; -} diff --git a/tests/_data/fixtures/src/EventFirstStoppable.php b/tests/_data/fixtures/src/EventFirstStoppable.php new file mode 100644 index 0000000..2037872 --- /dev/null +++ b/tests/_data/fixtures/src/EventFirstStoppable.php @@ -0,0 +1,24 @@ +propagationStopped = true; + } + + public function isPropagationStopped(): bool + { + return $this->propagationStopped; + } +} diff --git a/tests/_data/fixtures/src/EventForRenderer.php b/tests/_data/fixtures/src/EventForRenderer.php new file mode 100644 index 0000000..dfffad8 --- /dev/null +++ b/tests/_data/fixtures/src/EventForRenderer.php @@ -0,0 +1,15 @@ +rendered; + } +} diff --git a/tests/_data/fixtures/src/EventMayStopPropagation.php b/tests/_data/fixtures/src/EventMayStopPropagation.php new file mode 100644 index 0000000..c6967dc --- /dev/null +++ b/tests/_data/fixtures/src/EventMayStopPropagation.php @@ -0,0 +1,21 @@ +propagationStopped; + } + + public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} diff --git a/tests/_data/fixtures/src/ListenerCallable.php b/tests/_data/fixtures/src/ListenerCallable.php new file mode 100644 index 0000000..0edb02a --- /dev/null +++ b/tests/_data/fixtures/src/ListenerCallable.php @@ -0,0 +1,13 @@ +value = get_text(); + } +} diff --git a/tests/_data/fixtures/src/RendererAsEvent.php b/tests/_data/fixtures/src/RendererAsEvent.php new file mode 100644 index 0000000..370dd18 --- /dev/null +++ b/tests/_data/fixtures/src/RendererAsEvent.php @@ -0,0 +1,22 @@ +dispatcher = $dispatcher; + } + + public function render(): string + { + return $this->dispatcher->dispatch($this)->rendered; + } +} diff --git a/tests/_data/fixtures/src/RendererWithEvent.php b/tests/_data/fixtures/src/RendererWithEvent.php new file mode 100644 index 0000000..1d71b87 --- /dev/null +++ b/tests/_data/fixtures/src/RendererWithEvent.php @@ -0,0 +1,20 @@ +dispatcher = $dispatcher; + } + + public function render(): string + { + return $this->dispatcher->dispatch(new EventForRenderer())->render(); + } +} diff --git a/tests/_data/fixtures/src/SubscriberMock.php b/tests/_data/fixtures/src/SubscriberMock.php new file mode 100644 index 0000000..7b09e75 --- /dev/null +++ b/tests/_data/fixtures/src/SubscriberMock.php @@ -0,0 +1,30 @@ +provider_args = $provider_args; + } + + public function executeCallable(): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function getSubscribedEvents(): iterable + { + return $this->provider_args; + } +} diff --git a/tests/_support/FunctionalTester.php b/tests/_support/FunctionalTester.php deleted file mode 100644 index 83fbacd..0000000 --- a/tests/_support/FunctionalTester.php +++ /dev/null @@ -1,26 +0,0 @@ -get_subscribed_events() as $event_name => $parameters ) { - $this->add_subscriber_listener( $subscriber, $event_name, $parameters ); - } - } - - /** - * Adds the given subscriber listener to the list of event listeners - * that listen to the given event. - * - * @param Subscriber_Interface $subscriber - * @param string $event_name - * @param mixed $parameters - */ - private function add_subscriber_listener( Subscriber_Interface $subscriber, $event_name, $parameters ) { - if ( \is_string( $parameters ) ) { - $this->add_listener( $event_name, array( $subscriber, $parameters ) ); - } elseif ( \is_array( $parameters ) && isset( $parameters['function_to_add'] ) ) { - $this->add_listener( - $event_name, - array( $subscriber, $parameters['function_to_add'] ), - isset( $parameters['priority'] ) ? $parameters['priority'] : 10, - isset( $parameters['accepted_args'] ) ? $parameters['accepted_args'] : 1 - ); - } - } - - /** - * Adds the given event listener to the list of event listeners - * that listen to the given event. - * - * @param string $event_name - * @param callable $listener - * @param int $priority - * @param int $accepted_args - */ - public function add_listener( $event_name, $listener, $priority = 10, $accepted_args = 1 ) { - \add_filter( $event_name, $listener, $priority, $accepted_args ); - } - - /** - * Hard removing a method registerd by an anonimous object. - * - * From an idea of Tonia Mork of KnowTheCode - * @link https://knowthecode.io/ - * - * @example - * $event_manager->hard_remove_subscriber( 'wp', 'schedule_events', 10 ); - * - * @param string $event_name The event name. - * @param string $method_name The method name callback to remove. - * @param int $priority The priority of the callback. - * @param int $accepted_args The number of the accepted args. - */ - public function hard_remove_subscriber( $event_name, $method_name, $priority, $accepted_args = null ) { - - global $wp_filter; - - if ( ! isset( $wp_filter[ $event_name ][ $priority ] ) ) { - return; - } - - foreach ( (array) $wp_filter[ $event_name ][ $priority ] as $method_name_regstered => $value ) { - if ( \strpos( $method_name_regstered, $method_name ) !== false ) { - \remove_filter( $event_name, $method_name_regstered, $priority ); - } - } - } - - /** - * Removes an event subscriber. - * - * The event manager removes the given subscriber from the list of event listeners - * for all the events that it wants to listen to. - * - * @param Subscriber_Interface $subscriber - */ - public function remove_subscriber( Subscriber_Interface $subscriber ) { - foreach ( $subscriber->get_subscribed_events() as $event_name => $parameters ) { - $this->remove_subscriber_listener( $subscriber, $event_name, $parameters ); - } - } - - /** - * Adds the given subscriber listener to the list of event listeners - * that listen to the given event. - * - * @param Subscriber_Interface $subscriber - * @param string $event_name - * @param mixed $parameters - */ - private function remove_subscriber_listener( Subscriber_Interface $subscriber, $event_name, $parameters ) { - if ( \is_string( $parameters ) ) { - $this->remove_listener( $event_name, array( $subscriber, $parameters ) ); - } elseif ( \is_array( $parameters ) && isset( $parameters['function_to_add'] ) ) { - $this->remove_listener( - $event_name, - array( $subscriber, $parameters['function_to_add'] ), - isset( $parameters['priority'] ) ? $parameters['priority'] : 10 - ); - } - } - - /** - * Removes the given event listener from the list of event listeners - * that listen to the given event. - * - * @param string $event_name - * @param callable $listener - * @param int $priority - */ - public function remove_listener( $event_name, $listener, $priority = 10 ) { - \remove_filter( $event_name, $listener, $priority ); - // $this->plugin_api_manager->remove_callback( $event_name, $listener, $priority ); - } -} diff --git a/tests/_temp/Event/Subscriber_Interface.php b/tests/_temp/Event/Subscriber_Interface.php deleted file mode 100644 index 002a2c8..0000000 --- a/tests/_temp/Event/Subscriber_Interface.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -declare(strict_types=1); -// phpcs:ignoreFile -namespace ItalyStrap\Event; - -interface Subscriber_Interface { - /** - * Returns an array of events (hooks) that this subscriber wants to register with - * the Events Manager API. - * - * The array key is the name of the hook. The value can be: - * - * * The method name - * * An array with the method name and priority - * * An array with the method name, priority and number of accepted arguments - * - * For instance: - * - * array( - * // 'event_name' => 'method_name', - * 'italystrap_before_header' => 'render', - * ) - * array( - * // 'event_name' => 'method_name', - * 'italystrap_before_entry_content' => array( - * 'function_to_add' => 'render', - * 'priority' => 30, // Default 10 - * 'accepted_args' => 1 // Default 1 - * ), - * ); - * - * @return array - */ - public static function get_subscribed_events(); -} diff --git a/tests/_temp/Event/example.md b/tests/_temp/Event/example.md deleted file mode 100644 index 92bc29a..0000000 --- a/tests/_temp/Event/example.md +++ /dev/null @@ -1,163 +0,0 @@ - 'the_content', - // 'italystrap_before_entry_content' => array( $template_settings, 'title' ), - - // $tag - event name - // 'italystrap_before_entry_content' => array( - // 'function_to_add' => array( $template_settings, 'title' ), - // // 'priority' => 10, - // // 'accepted_args' => null, - // ), - 'italystrap_after_entry_content' => array( - array( - 'function_to_add' => array( $template_settings, 'title' ), - 'priority' => 10, - 'accepted_args' => null, - ), - array( - 'function_to_add' => array( $template_settings, 'content' ), - // 'priority' => 10, - // 'accepted_args' => null, - ), - ), - -); - -$event_manager = new Event_Manager(); -$event_manager->add_subscriber( $class ); -$event_manager->remove_subscriber( $class ); -$event_manager->hard_remove_subscriber( 'some_hook', 'some_method_name', 10 ); -// $events = array( - -// // 'italystrap_before_entry_content' => 'the_content', -// // 'italystrap_before_entry_content' => array( $template_settings, 'title' ), - -// // $tag - event name -// // 'italystrap_before_entry_content' => array( -// // 'function_to_add' => array( $template_settings, 'title' ), -// // // 'priority' => 10, -// // // 'accepted_args' => null, -// // ), -// 'italystrap_after_entry_content' => array( -// array( -// 'function_to_add' => array( $template_settings, 'title' ), -// // 'priority' => 10, -// // 'accepted_args' => null, -// ), -// array( -// 'function_to_add' => array( $template_settings, 'content' ), -// // 'priority' => 10, -// // 'accepted_args' => null, -// ), -// ), - -// ); - -$events_manager = new Event_Manager(); -// $events_manager->add_events( $events ); - -use ItalyStrap\Event\Subscriber_Interface; - -/** - * Class description - */ -class ClassName extends AnotherClass implements Subscriber_Interface { - - /** - * Returns an array of hooks that this subscriber wants to register with - * the WordPress plugin API. - * - * @hooked 'wp_footer' - 20 - * - * @return array - */ - public static function get_subscribed_events() { - - return array( - // 'hook_name' => 'method_name', - 'wp_footer' => array( - 'function_to_add' => 'lazy_load_fonts', - 'priority' => 9999, // Optional - 'accepted_args' => null, // Optional - ), - ); - } -} - -use ItalyStrap\Event\Subscriber_Interface; - -/** - * Class description - */ -class ClassName extends AnotherClass implements Subscriber_Interface { - - /** - * Returns an array of hooks that this subscriber wants to register with - * the WordPress plugin API. - * - * @hooked 'wp_footer' - 20 - * - * @return array - */ - public static function get_subscribed_events() { - - return array( - // 'hook_name' => 'method_name', - 'wp_footer' => 'lazy_load_fonts', - ); - } -} - -use ItalyStrap\Event\Subscriber_Interface; - -/** - * Class description - */ -class ClassName extends AnotherClass implements Subscriber_Interface { - - /** - * Returns an array of hooks that this subscriber wants to register with - * the WordPress plugin API. - * - * @hooked 'wp_footer' - 20 - * - * @return array - */ - public static function get_subscribed_events() { - - return array( - // 'hook_name' => 'method_name', - 'wp_footer' => array( - 'function_to_add' => 'lazy_load_fonts', - 'priority' => 9999, // Optional - 'accepted_args' => null, // Optional - ), - 'wp_footer' => 'lazy_load_fonts', - ); - } -} diff --git a/tests/_temp/Event/index.php b/tests/_temp/Event/index.php deleted file mode 100644 index 5d69378..0000000 --- a/tests/_temp/Event/index.php +++ /dev/null @@ -1,2 +0,0 @@ -makeInstance(); + + $actual = $sut->dispatch($event); + Assert::assertSame($event, $actual, 'It should return the same event'); + } +} diff --git a/tests/integration/DispatcherTest.php b/tests/integration/DispatcherTest.php new file mode 100644 index 0000000..73651fa --- /dev/null +++ b/tests/integration/DispatcherTest.php @@ -0,0 +1,401 @@ +makeDispatcher( + new class implements ListenerProviderInterface { + public function getListenersForEvent(object $event): iterable + { + return []; + } + } + ); + + $event = new \stdClass(); + + $isCalled = false; + \add_filter( + \stdClass::class, + function (object $event) use (&$isCalled) { + $event->value = 42; + $isCalled = true; + } + ); + + $actual = $sut->dispatch($event); + Assert::assertSame($event, $actual, 'The event should be the same'); + Assert::assertFalse($isCalled, 'The event should not be called'); + + \do_action(\stdClass::class, $event); + Assert::assertTrue(\property_exists($event, 'value'), 'The event should have the property value'); + Assert::assertTrue($isCalled, 'The event should not be called'); + } + + public function testItShouldDispatchEvent() + { + $provider = $this->makeListenerProvider(); + + $isCalled = false; + $provider->addListener( + \stdClass::class, + function (object $event) use (&$isCalled) { + Assert::assertTrue(\doing_filter(\current_filter())); + $isCalled = true; + } + ); + + $sut = $this->makeDispatcher( + $provider + ); + + $event = new \stdClass(); + $sut->dispatch($event); + Assert::assertTrue($isCalled, 'The event should be called'); + } + + public function testCurrentEventNameItShouldMatchTheEventObject() + { + $provider = $this->makeListenerProvider(); + + $provider->addListener( + \stdClass::class, + function (object $event) { + Assert::assertSame(\stdClass::class, \current_filter()); + Assert::assertSame(\get_class($event), \current_filter()); + $event->value = 42; + } + ); + + $sut = $this->makeDispatcher( + $provider + ); + + $event = new \stdClass(); + $sut->dispatch($event); + Assert::assertTrue(\property_exists($event, 'value'), 'The event should have the property value'); + } + + public function testItShouldDispatchEvent2() + { + $provider = $this->makeListenerProvider(); + + $provider->addListener( + \stdClass::class, + function (object $event) { + $event->value = 42; + } + ); + + $provider->addListener( + \stdClass::class, + function (object $event) { + $event->value = 42 ** 2; + }, + 9 + ); + + \add_filter( + \stdClass::class, + function (object $event) { + $event->newValue = 84; + } + ); + + $event = new \stdClass(); + + $sut = $this->makeDispatcher( + $provider + ); + + $actual = $sut->dispatch($event); + + Assert::assertSame(42, $event->value, 'The event value should be 42'); + Assert::assertSame(84, $event->newValue, 'The event value should be 84'); + Assert::assertTrue((int)\did_action(\stdClass::class) > 0, 'The action should be called'); + Assert::assertTrue(\has_action(\stdClass::class), 'The action should be registered'); + } + + public function testItShouldStopPropagation() + { + $provider = $this->makeListenerProvider(); + + $event = new EventMayStopPropagation(); + + $eventName = \get_class($event); + + $provider->addListener( + $eventName, + function (object $event) { + $event->value = 42; + \assert(\method_exists($event, 'stopPropagation')) and $event->stopPropagation(); + } + ); + + $provider->addListener( + $eventName, + function (object $event) { + $event->value = 42 ** 2; + } + ); + + $dispatcher = $this->makeDispatcher( + $provider + ); + + $actual = $dispatcher->dispatch($event); + + Assert::assertSame(42, $actual->value, 'The event value should be 42'); + } + + public function testItShouldAddFunctionListenerAndChangeValue() + { + + $provider = $this->makeListenerProvider(); + + $sut = $this->makeDispatcher( + $provider + ); + + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + + $event = $sut->dispatch(new EventFirstStoppable()); + + Assert::assertEquals(42, $event->value, ''); + Assert::assertFalse($event->isPropagationStopped(), 'It should not stop propagation'); + } + + public function testItShouldRemoveFunctionListenerAndReturnValueWithoutChanges() + { + + $provider = $this->makeListenerProvider(); + + $sut = $this->makeDispatcher(new GlobalOrderedListenerProvider()); + + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + $provider->removeListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + + /** @var object $event */ + $event = $sut->dispatch(new EventFirstStoppable()); + + Assert::assertEquals(0, $event->value, ''); + Assert::assertFalse($event->isPropagationStopped(), 'It should not stop propagation'); + } + + public function testItShouldStopPropagationWithMoreListener() + { + + $provider = new GlobalOrderedListenerProvider(); + + $sut = $this->makeDispatcher($provider); + + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + $provider->addListener( + EventFirstStoppable::class, + [new ListenerChangeValueToText(), 'changeText' ] + ); + + // Here it will set value to false and stop propagation + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_false_and_stop_propagation' + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_77' + ); + + + $event = new EventFirstStoppable(); + + /** @var object $event */ + $sut->dispatch($event); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + } + + public function testItShouldRemoveListenerAndReturnValue77() + { + $provider = new GlobalOrderedListenerProvider(); + + $sut = $this->makeDispatcher($provider); + + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + $provider->addListener( + EventFirstStoppable::class, + [new ListenerChangeValueToText(), 'changeText' ] + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_false_and_stop_propagation' + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_77' + ); + + $provider->removeListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_false_and_stop_propagation' + ); + + $event = new EventFirstStoppable(); + + /** @var object $event */ + $sut->dispatch($event); + + Assert::assertEquals(77, $event->value, ''); + Assert::assertFalse($event->isPropagationStopped(), ''); + } + + public function testIfSameEventIsDispatchedMoreThanOnceItShouldStopPropagationIfListenerStopPropagation() + { + $provider = new GlobalOrderedListenerProvider(); + + $sut = $this->makeDispatcher($provider); + + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + $provider->addListener( + EventFirstStoppable::class, + [new ListenerChangeValueToText(), 'changeText' ] + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_false_and_stop_propagation' + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_77' + ); + + $event = new EventFirstStoppable(); + + /** @var object $event */ + $event = $sut->dispatch($event); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + + $event = $sut->dispatch(new EventFirstStoppable()); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + } + + public function testIfSameEventIsDispatchedMoreThanOnceItShouldStopPropagationIfListenerStopPropagationWithSymfony() + { + $sut = new EventDispatcher(); + + $sut->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + $sut->addListener( + EventFirstStoppable::class, + [new ListenerChangeValueToText(), 'changeText' ] + ); + $sut->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_false_and_stop_propagation' + ); + $sut->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_77' + ); + + $event = new EventFirstStoppable(); + + /** @var object $event */ + $event = $sut->dispatch($event); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + + $event = $sut->dispatch(new EventFirstStoppable()); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + } + + public function testCallDispatchTwoTimesWithSameEvent() + { + $provider = new GlobalOrderedListenerProvider(); + + $sut = $this->makeDispatcher($provider); + + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_42' + ); + $provider->addListener( + EventFirstStoppable::class, + [new ListenerChangeValueToText(), 'changeText' ] + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_false_and_stop_propagation' + ); + $provider->addListener( + EventFirstStoppable::class, + 'ItalyStrap\Tests\listener_change_value_to_77' + ); + + $event = new EventFirstStoppable(); + + /** @var object $event */ + $event = $sut->dispatch($event); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + + $event = $sut->dispatch($event); + + Assert::assertEquals(false, $event->value, ''); + Assert::assertTrue($event->isPropagationStopped(), ''); + } +} diff --git a/tests/integration/EventSubscriptionTest.php b/tests/integration/EventSubscriptionTest.php new file mode 100644 index 0000000..71289fe --- /dev/null +++ b/tests/integration/EventSubscriptionTest.php @@ -0,0 +1,23 @@ +callback, + $this->priority, + $this->acceptedArgs + ); + } +} diff --git a/tests/integration/GlobalDispatcherTest.php b/tests/integration/GlobalDispatcherTest.php new file mode 100644 index 0000000..63c348b --- /dev/null +++ b/tests/integration/GlobalDispatcherTest.php @@ -0,0 +1,48 @@ +makeDispatcher(); + $listenerRegister = new GlobalOrderedListenerProvider(); + + $listenerRegister->addListener('event_name', function () { + echo 'Value printed'; + }); + + $this->expectOutputString('Value printed'); + $sut->trigger('event_name'); + } + + public function testClassWithDispatchDependency() + { + $sut = $this->makeDispatcher(); + $listenerRegister = new GlobalOrderedListenerProvider(); + + $some_class = new ClassWithDispatchDependency($sut); + + $listenerRegister->addListener( + ClassWithDispatchDependency::EVENT_NAME, + fn(string $value) => 'New value' + ); + + $some_class->filterValue(); + + $this->assertStringContainsString('New value', $some_class->value(), ''); + } +} diff --git a/tests/integration/GlobalOrderedListenerProviderTest.php b/tests/integration/GlobalOrderedListenerProviderTest.php new file mode 100644 index 0000000..12e27da --- /dev/null +++ b/tests/integration/GlobalOrderedListenerProviderTest.php @@ -0,0 +1,19 @@ +createMock(EventDispatcherInterface::class); + } + + public function makeInstance(): GlobalState + { + return new GlobalState(); + } +} diff --git a/tests/integration/ImplementationTest.php b/tests/integration/ImplementationTest.php new file mode 100644 index 0000000..8676278 --- /dev/null +++ b/tests/integration/ImplementationTest.php @@ -0,0 +1,764 @@ += (major or equal) to the number of arguments passed to the function + * will be used the `call_user_func_array()` like this `call_user_func_array( $the_['function'], $args )`. + * + * Now this means that you could use any number major or equal to the number accepted by the callback + * even PHP_MAX_INT if you want, `call_user_func_array()` will not complain about that. + * + * If you want to call `call_user_func()` you need to add 0 as the `$accepted_args` argument. + * This happens if you for example dispatch an event with only the event name and no other arguments. + * `do_action('event_name');` or `apply_filters('event_name');` + * + * (Even if in this case they do the same thing it is better to call `do_action()` to dispatch this event, + * a case could be to run some code in the stack without the need to have a value, loggin, echoing, and other + * kind of side effects.) + * + * If the number is not 0 and is < (minor) to the number of arguments passed to the function + * will be used the `call_user_func_array()` like this + * `call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) )` a little + * more expensive than the previous one. + * + * So, after this long comment let's see how we can register a listener to a specific event name (hook) and + * how we can dispatch an event. + */ + public function testRegisterAndDispatchingTheWordPressWay() + { + // Let's register a listener to a specific event name (hook) 'event_name_for_filter' + // As I said before we can use `add_action()` or `add_filter()` to do the same thing. + // so let's use `add_filter()`. + \add_filter( + 'event_name_for_filter', // The name of the event to which the $callback is hooked. + // The callback to be run when the event is fired. + function (string $value) { + // For this example the callback accept only one argument + // Because we use `apply_filters()` we need to return the value + return $value . ' World'; // In this example we just append ' World' to the value we received + } + ); + + // Now we can dispatch the event + $value = \apply_filters('event_name_for_filter', 'Hello'); + // And we assert that the value is equal to 'Hello World' + Assert::assertSame('Hello World', $value); + // Normally `apply_filters()` is used in functions that return a value like for example `the_title()` + + // Now let's register another listener to the event name (hook) 'event_name_for_action' + // but this time we do not return the value because we use `do_action()` later. + \add_filter( + 'event_name_for_action', // The name of the event to which the $callback is hooked. + // The callback to be run when the event is fired. + function (string $value) { + // For this example the callback accept only one argument + echo $value . ' World'; // In this example we just append ' World' to the value we received + // Because we use `do_action()` we do not need to return the value + // In this example we do side effects like echoing. + } + ); + + $arg = 'Hello'; + // We need to sniff the output of the callback to see if it works as expected + $this->expectOutputString('Hello World'); + \do_action('event_name_for_action', $arg); + // Normally `do_action()` is used in functions that do not return a value like for example `wp_head()` + // or in case you need to perform some saving or other side effects. + // in pseudocode: + // if $args is equal to 'Hello' then do stuff + // The `$args` is not changed in this case because the value was a string and not an object. + Assert::assertSame('Hello', $arg); + + // This will happen with all type of values passed to the callback but not with objects. + // Objects are passed by reference so if you change the object in the callback the object will be changed + // also in the caller (the caller is the function that dispatch the event). + // Let's see an example: + $arg = new \stdClass(); + $arg->name = 'Hello'; + \add_filter( + 'event_name_for_action_with_object', // The name of the event to which the $callback is hooked. + // The callback to be run when the event is fired. + function (\stdClass $value) { + // For this example the callback accept only one argument + $value->name .= ' World'; // In this example we just append ' World' to the value we received + // Because we use `do_action()` we do not need to return the value + // In this example we do side effects like echoing. + } + ); + \do_action('event_name_for_action_with_object', $arg); + // Now the $arg->name is changed because the object is passed by reference. + Assert::assertSame('Hello World', $arg->name); + + // We can do the same with `apply_filters()` and objects. + $arg = new \stdClass(); + $arg->name = 'Hello'; + \add_filter( + 'event_name_for_filter_with_object', // The name of the event to which the $callback is hooked. + // The callback to be run when the event is fired. + function (\stdClass $value) { + // For this example the callback accept only one argument + $value->name .= ' World'; // In this example we just append ' World' to the value we received + // Because we use `do_action()` we do not need to return the value + // In this example we do side effects like echoing. + return $value; + } + ); + $arg = \apply_filters('event_name_for_filter_with_object', $arg); + // Now the $arg->name is changed because the object is passed by reference. + Assert::assertSame('Hello World', $arg->name); + + // Fun fact: because we use an object and `apply_filters()` return a value we could do something like this: + Assert::assertSame( + 'Hello World World', // This is the expected value because we have two listeners in the stack + \apply_filters('event_name_for_filter_with_object', $arg)->name + ); + + // But because `apply_filters()` can return any type of value (even unicorns Cit.) not only objects + // please do not do this until you know what you are doing, but still, + // do not do this even if you know what you're doing because let's say that you have a listener that return a + // string and another listener that return an object, what will happen? + // Let's see: + \add_filter( + 'event_name_for_filter_with_object', + function (string $value) { + return 'Something that is not an object'; + } + ); + + // I need to wrap this in a closure and do assertion to avoid TypeError + $this->tester->expectThrowable( + \TypeError::class, + function () use ($arg) { + // This will throw a TypeError because the first listener return a string and not an object + // as we could expect. + \apply_filters('event_name_for_filter_with_object', $arg)->name; + } + ); + + // The later example was only an introduction to what we can do with the PSR-14 implementation, but + // I'll explain more later. + } + + /** + * In this test wa are not using any implementation of the StateInterface + * this means that `current_filter()`, `doing_filter` and `did_action()` will return a default value + * because the globals declared by WordPress are not set. + * + * Pay attention if you call the WordPress Hooks API `\do_action()` + * after the `::dispatch()` method like in this example: + * `\do_action(\stdClass::class, $event);` + * the globals will be set and the test will fail. + */ + public function testDispatcherStatelessSimpleImplementation(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + + $event = new \stdClass(); + + $listenerProvider->addListener(\stdClass::class, function (object $event) { + Assert::assertEmpty(\current_filter(), 'Current filter should be empty'); + Assert::assertFalse(\doing_filter(\stdClass::class), 'Doing action should return false'); + Assert::assertSame(0, \did_action(\stdClass::class), 'Did action should return 0'); + $event->name = 'Hello'; + }); + + $listenerProvider->addListener(\stdClass::class, function (object $event) { + Assert::assertEmpty(\current_filter(), 'Current filter should be empty'); + Assert::assertFalse(\doing_filter(\stdClass::class), 'Doing action should return false'); + Assert::assertSame(0, \did_action(\stdClass::class), 'Did action should return 0'); + $event->name .= ' World'; + }, 20); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $name = (string)$dispatcher->dispatch($event)->name; + + Assert::assertSame('Hello World', $name); + Assert::assertSame(0, \did_action(\stdClass::class), 'Did action should return 0'); + } + + /** + * In this test we are using the GlobalState implementation of the StateInterface + * this means that `current_filter()`, `doing_filter` and `did_action()` will return the correct value + * because the globals declared by WordPress are set. + * This is the same as the previous test but with the GlobalState implementation. + * + * Now if you call the WordPress Hooks API `\do_action()` + * after the `::dispatch()` method like in this example: + * `\do_action(\stdClass::class, $event);` + * the `did_action()` will be incremented as expected. + * + * In this case both API act in the same way. + */ + public function testDispatcherWithGlobalStateImplementation(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $state = new \ItalyStrap\Event\GlobalState(); + + $event = new \stdClass(); + + $listenerProvider->addListener(\stdClass::class, function (object $event) { + $event->name = 'Hello'; + }); + + $listener = new class ($state) { + private \ItalyStrap\Event\GlobalState $state; + + public function __construct(\ItalyStrap\Event\GlobalState $state) + { + $this->state = $state; + } + + public function __invoke(object $event) + { + Assert::assertSame( + \get_class($event), + \current_filter(), + 'Current event name should be equal to the event name' + ); + Assert::assertTrue( + \doing_filter(\get_class($event)), + 'Doing action should return true' + ); + + Assert::assertSame( + \current_filter(), + $this->state->currentEventName(), + 'Current event name should be equal to the current filter' + ); + + Assert::assertSame( + \doing_filter(\get_class($event)), + $this->state->isDispatching(), + 'Doing filter should be equal to the isDispatching' + ); + + $event->name .= ' World'; + $event->currentState = $this->state->currentEventName(); + } + }; + + $listenerProvider->addListener(\stdClass::class, $listener, 20); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider, $state); + + + $name = $dispatcher->dispatch($event)->name; + + Assert::assertSame('Hello World', $name, 'Expected name should be equal to Hello World'); + Assert::assertSame('stdClass', $event->currentState); + Assert::assertSame(1, \did_action(\stdClass::class), 'Did action should return 1'); + Assert::assertSame(1, $state->dispatchedEventCount(), 'Did action should return 1'); + + \do_action(\stdClass::class, $event); + + Assert::assertSame('Hello World', $name, 'Expected name should be equal to Hello World'); + Assert::assertSame(2, \did_action(\stdClass::class), 'Did action should return 2'); + Assert::assertSame(2, $state->dispatchedEventCount(), 'Did action should return 2'); + } + + /** + * In this test is similar to the previous one but here we're using the StoppableEventInterface + * As you can see the StoppableEventInterface works only with the PSR-14 implementation + * Calling `do_action()` an event that implements the StoppableEventInterface will not stop further execution + * because the `do_action()` is not aware of the StoppableEventInterface. + * `do_action()` will continue to execute all the listeners in the stack. + */ + public function testDispatcherWithGlobalImplementationAndStoppableImplementation(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $state = new \ItalyStrap\Event\GlobalState(); + + $event = new class implements StoppableEventInterface { + use \ItalyStrap\Event\PropagationAwareTrait; + + public string $name = ''; + }; + + $eventName = \get_class($event); + + $listenerProvider->addListener($eventName, function (object $event) { + $event->name = 'Hello'; + }, 10); + + $listenerProvider->addListener($eventName, function (object $event) { + \method_exists($event, 'stopPropagation') and $event->stopPropagation(); + }, 11); + + $listenerProvider->addListener($eventName, function (object $event) { + $event->name .= ' World'; + }, 12); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider, $state); + + $name = $dispatcher->dispatch($event)->name; + + Assert::assertSame('Hello', $name, 'The event should be stopped'); + Assert::assertSame(1, $state->dispatchedEventCount(), 'Did action should return 1'); + + $name = $dispatcher->dispatch($event)->name; + + Assert::assertSame('Hello', $name, 'The event should be stopped'); + Assert::assertSame(2, $state->dispatchedEventCount(), 'Did action should return 2'); + + \do_action($eventName, $event); + + Assert::assertSame('Hello World', $event->name, 'The event should not be stopped'); + Assert::assertSame(3, $state->dispatchedEventCount(), 'Did action should return 3'); + } + + /** + * Example is taken here https://gist.github.com/westonruter/6647252 + * In this test we can see an example on how you can remove and add again a filter + * to some entity in WordPress, this is needed because you do not want to have the changes + * in all event names but only in one specific event, so the other events will not be affected. + */ + public function testAddAndRemoveFilterInWordPressEnv() + { + // I add this because in the test env the `wptexturize` filter is not yet added. + \add_filter('the_title', 'wptexturize'); + + // Just create a Fake post + $entityId = \wp_insert_post( + [ + 'post_title' => "'cause today's effort makes it worth tomorrow's \"holiday\"", + 'post_content' => 'Hello World', + ] + ); + + // Make sure the filter is added + Assert::assertSame( + "’cause today’s effort makes it worth tomorrow’s “holiday”", + \get_the_title($entityId) + ); + + \remove_filter('the_title', 'wptexturize'); + $title = get_the_title($entityId); + Assert::assertSame("'cause today's effort makes it worth tomorrow's \"holiday\"", $title); + \add_filter('the_title', 'wptexturize'); + + // Add again the filter to revert the default behaviour + $title = get_the_title($entityId); + Assert::assertSame( + "’cause today’s effort makes it worth tomorrow’s “holiday”", + \get_the_title($entityId) + ); + } + + /** + * We can do the same as the previous test but using the PSR-14 implementation + * For this test I use a Renderer but the same can be done with any other events. + * Take a look at this test, as you can see we're trying to remove a listener + * to revert the behaviour of the renderer but because the $mockRenderer is passed by reference + * even if we remove the listener the object s already modified and passed as is. + * If we want to remove a listener we need to instantiate a new RendererAsEvent object. + */ + public function testAddAndRemoveListenerForRendererAsEvent(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $mockRenderer = new RendererAsEvent($dispatcher); + + Assert::assertSame('Hello World', $mockRenderer->render()); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListener(\get_class($mockRenderer), $listener, 10); + + Assert::assertSame('Hello there', $mockRenderer->render()); + + // This won't take effect because the $mockRenderer is passed by reference because it's an object. + $listenerProvider->removeListener(\get_class($mockRenderer), $listener); + + Assert::assertSame('Hello there', $mockRenderer->render()); + } + + /** + * This example is similar to the previous one but in this case we created a dedicated event to be + * instantiated inside the render method of the RendererWithEvent class. + * In this case because the event is instantiated every time the render method is called + * if we remove the listener we can revert the behaviour of the RendererWithEvent::render() method as the default. + */ + public function testAddAndRemoveListenerForRendererWithEvent(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $mockRenderer = new RendererWithEvent($dispatcher); + + Assert::assertSame('Hello World', $mockRenderer->render()); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListener(EventForRenderer::class, $listener); + + Assert::assertSame('Hello there', $mockRenderer->render()); + + // This time it will take effect because EventForRenderer::class in instantiated inside the dispatcher + // every time the dispatch() method is called. + $listenerProvider->removeListener(EventForRenderer::class, $listener); + + Assert::assertSame('Hello World', $mockRenderer->render()); + } + + /** + * This example shows a different way to use the dispatcher connected to the WordPress Hooks API. + * The simplest way is to register the listener with the `add_filter()` function, in this example + * we're using an external package to register the listener, we extend it, and we use the `add_filter()` function + * inside the `addListener()` method. + * This way we can be sure our listener is registered both in + * the WordPress Hooks API and in the PSR-14 implementation. + * + * With this method we can use both `\do_action()` and `$dispatcher->dispatch()` to trigger the event. + * But + */ + public function testWordPressListenerWithExternalPackage(): void + { + $listenerProvider = new class extends OrderedListenerProvider implements ListenerProviderInterface { + public function addListenerFromCallable( + callable $listener, + ?int $priority = null, + ?string $id = null, + ?string $type = null + ): string { + \add_filter( + $type, + $listener, + $priority ?? 10, + ); + return parent::addListener($listener, $priority, $id, $type); + } + }; + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $event = new EventForRenderer(); + + Assert::assertSame('Hello World', $event->rendered); + + $listener = function (object $event): void { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListenerFromCallable($listener, null, null, EventForRenderer::class); + + Assert::assertSame('Hello there', $dispatcher->dispatch($event)->rendered); + + $event = new EventForRenderer(); + \do_action(EventForRenderer::class, $event); + Assert::assertSame('Hello there', $event->rendered); + + $event = new EventForRenderer(); + $value = \apply_filters(EventForRenderer::class, $event); + Assert::assertSame('Hello there', $event->rendered); + // Because the $listener callback does not return a value the $value will be null + Assert::assertNull($value, 'The return value of the filter should be null'); + } + + /** + * If you want to use string event name you can still do it with `addListener()` method + * because the `addListener()` method use the `add_filter()` function to register the listener, + * but pay attention, if you want to dispatch the event you need to use one of the WordPress Hooks API + * `do_action()` or `apply_filters()`. + * + * The simple explanation is that the `dispatch()` method is only aware of the event as object + * and under the hood when loop the stack of listeners only a listener that match + * the event object name will be executed. + */ + public function testAddListenerForRendererWithEventString(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListener('event_name', $listener); + + $event = new EventForRenderer(); + \do_action('event_name', $event); + + Assert::assertSame('Hello there', $event->rendered); + + $dispatcher->dispatch($event); + // As you can see the `dispatch()` method does not change the event. + Assert::assertSame('Hello there', $event->rendered); + } + + /** + * Now this example shows the possibility to use an alias for the event name, so instead of using + * the object event name you can use a string event name and bind it to the object event name. + * Right now is still experimental, need to be tested more. + * + * But this could be dangerous because if you bind for example an event name to a string that is already + * used by classic `do_action` or `apply_filters()` you could have some unexpected behaviour, just to name a few: + * - the_title + * - the_content + * - the_excerpt + * and so on. + */ + public function testAddListenerForRendererWithEventStringAsAlias(): void + { + $listenerProvider = new class implements ListenerProviderInterface { + private ListenerProviderInterface $listenerProvider; + private array $aliases = []; + + public function __construct() + { + $this->listenerProvider = new GlobalOrderedListenerProvider(); + } + + public function alias(string $alias, string $eventName): void + { + $this->aliases[$eventName] = $alias; + } + + public function addListener(string $eventName, callable $listener, int $priority = 10): bool + { + return $this->listenerProvider->addListener($eventName, $listener, $priority); + } + + public function getListenersForEvent(object $event): iterable + { + global $wp_filter; + $callbacks = []; + $eventName = \get_class($event); + $eventName = $this->aliases[$eventName] ?? $eventName; + + if (!\array_key_exists($eventName, $wp_filter)) { + return $callbacks; + } + + if (!$wp_filter[$eventName] instanceof \WP_Hook) { + return $callbacks; + } + + foreach ($wp_filter[$eventName]->callbacks as $callbacks) { + foreach ($callbacks as $callback) { + yield $callback['function']; + } + } + } + }; + + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->alias('event_name', EventForRenderer::class); + $listenerProvider->addListener('event_name', $listener); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + // This event name is aliased so calling the `do_action()` with alias name will change the event + \do_action('event_name', $event); + // As you can see the event is changed + Assert::assertSame('Hello there', $event->rendered); + + // Revert the event name to the original + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + // Because is aliased now the `dispatch()` method is triggered. + $dispatcher->dispatch($event); + // And the event is changed + Assert::assertSame('Hello there', $event->rendered); + + /** + * Just a reminder, because we add an alias name to `add_filter()` + * `do_action()` and `apply_filters()` know only the alias name and not the original object event name. + */ + } + + public function testAggregateProvider(): void + { + $aggregateProvider = new AggregateProvider(); + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $aggregateProvider->addProvider($listenerProvider); + $dispatcher = new \ItalyStrap\Event\Dispatcher($aggregateProvider); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + $listener = function (object $event) { + $event->rendered = 'Hello there'; + }; + + $listenerProvider->addListener(EventForRenderer::class, $listener); + + $dispatcher->dispatch($event); + Assert::assertSame('Hello there', $event->rendered); + } + + public function testTryToCreateBridgeBetweenEventNameAndPSR14(): void + { + $title = \apply_filters('the_title', 'Hello World'); + + Assert::assertSame('Hello World', $title); + + $theTitleEvent = new class ('Hello World') { + public string $title; + + public function __construct(string $title) + { + $this->title = $title; + } + + public function __toString() + { + return \apply_filters('the_title', $this->title, \get_the_ID()); + } + }; + + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $listener = function (object $event) { + $event->title = 'Hello Universe'; + }; + + $listenerProvider->addListener(\get_class($theTitleEvent), $listener); + + $title = (string)$dispatcher->dispatch($theTitleEvent); + + Assert::assertSame('Hello Universe', $title); + + $customTitleEvent = new class ('Hello World') { + public const EVENT_NAME = 'custom_title'; + + public string $title; + + public function __construct(string $title) + { + $this->title = $title; + } + + public function __toString() + { + return $this->title; + } + }; + + $titleEvent = (object)\apply_filters($customTitleEvent::EVENT_NAME, $customTitleEvent); + + Assert::assertSame('Hello World', $titleEvent->title); + } + + public function testApplyFiltersAndDoActionApproachWithObjectEvent(): void + { + $listenerProvider = new \ItalyStrap\Event\GlobalOrderedListenerProvider(); + $dispatcher = new \ItalyStrap\Event\Dispatcher($listenerProvider); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + /** + * If we use object for the classic Hook WordPress API and we want to use + * in particular the `apply_filters()` remember that the listener + * must return the value. + */ + $listener = function (object $event): object { + $event->rendered = 'Hello there'; + return $event; + }; + + $listenerProvider->addListener(EventForRenderer::class, $listener); + + $dispatcher->dispatch($event); + Assert::assertSame('Hello there', $event->rendered); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + \do_action(EventForRenderer::class, $event); + Assert::assertSame('Hello there', $event->rendered); + + $event = new EventForRenderer(); + Assert::assertSame('Hello World', $event->rendered); + + $event = (object)\apply_filters(EventForRenderer::class, $event); + Assert::assertSame('Hello there', $event->rendered); + } +} diff --git a/tests/integration/PropagationTest.php b/tests/integration/PropagationTest.php new file mode 100644 index 0000000..3719974 --- /dev/null +++ b/tests/integration/PropagationTest.php @@ -0,0 +1,21 @@ +makeInstance(); + $subscriber = new SubscriberMock($provider_args); + $sut->addSubscriber($subscriber); + } + + /** + * @dataProvider subscriberProvider() + */ + public function testItShouldRemoveSubscriberWith($provider_args): void + { + $sut = $this->makeInstance(); + $subscriber = new SubscriberMock($provider_args); + $sut->removeSubscriber($subscriber); + } +} diff --git a/tests/integration/SubscribersConfigExtensionTest.php b/tests/integration/SubscribersConfigExtensionTest.php new file mode 100644 index 0000000..ed74aa5 --- /dev/null +++ b/tests/integration/SubscribersConfigExtensionTest.php @@ -0,0 +1,58 @@ +share($injector); + + $injector->alias(GlobalDispatcherInterface::class, GlobalDispatcher::class); + $injector->alias(ListenerRegisterInterface::class, GlobalOrderedListenerProvider::class); + $injector->share(GlobalDispatcherInterface::class); + $injector->share(SubscriberRegister::class); + $injector->defineParam('provider_args', [ + 'event' => function () { + echo 'Some text'; + }, + ]); + + $event_resolver = $injector->make(SubscribersConfigExtension::class, [ + ':config' => ConfigFactory::make([ + SubscriberMock::class => false + ]), + ]); + + $dependencies = ConfigFactory::make([ + SubscribersConfigExtension::SUBSCRIBERS => [ + SubscriberMock::class, + ], + ]); + + $empress = $injector->make(AurynResolver::class, [ + ':dependencies' => $dependencies + ]); + $empress->extend($event_resolver); + $empress->resolve(); + + $this->expectOutputString('Some text'); + ( $injector->make(GlobalDispatcher::class) )->trigger('event'); + } +} diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php new file mode 100644 index 0000000..2d19a0d --- /dev/null +++ b/tests/integration/bootstrap.php @@ -0,0 +1,16 @@ +callback = function () { + }; + $sut = $this->makeInstance(); + $this->assertInstanceOf(EventSubscription::class, $sut); + } + + public function testItShouldReturnCorrectArrayWithCorrectKeyValuesEqualsToConstructorArguments() + { + $this->callback = function () { + }; + $this->priority = 20; + $this->acceptedArgs = 2; + + $sut = $this->makeInstance(); + $this->assertIsArray($sut->toArray()); + $this->assertArrayHasKey(SubscriberInterface::CALLBACK, $sut->toArray()); + $this->assertArrayHasKey(SubscriberInterface::PRIORITY, $sut->toArray()); + $this->assertArrayHasKey('accepted_args', $sut->toArray()); + $this->assertSame($this->callback, $sut->toArray()[SubscriberInterface::CALLBACK]); + $this->assertSame($this->priority, $sut->toArray()[SubscriberInterface::PRIORITY]); + $this->assertSame($this->acceptedArgs, $sut->toArray()[SubscriberInterface::ACCEPTED_ARGS]); + } +} diff --git a/tests/src/GlobalStateTestTrait.php b/tests/src/GlobalStateTestTrait.php new file mode 100644 index 0000000..f8de69e --- /dev/null +++ b/tests/src/GlobalStateTestTrait.php @@ -0,0 +1,106 @@ +makeInstance(); + Assert::assertEmpty( + $sut->currentEventName(), + 'Current event should be empty if forEvent() is not called' + ); + + Assert::assertFalse( + $sut->isDispatching(), + 'Should be false if forEvent() is not called' + ); + + Assert::assertSame( + 0, + $sut->dispatchedEventCount(), + 'Dispatched event count should be 0 if forEvent() is not called' + ); + } + + public function testGlobalStateWithCallingForEvent() + { + $sut = $this->makeInstance(); + + $sut->forEvent(new \stdClass(), $this->makeDispatcher()); + $sut->progress(StateInterface::BEFORE, $this->makeDispatcher()); + + Assert::assertSame( + 1, + $sut->dispatchedEventCount(), + 'Dispatched event count should be 0 if forEvent() is called' + ); + + Assert::assertSame( + 'stdClass', + $sut->currentEventName(), + 'Current event should be foo if forEvent() is called with foo' + ); + + Assert::assertTrue( + $sut->isDispatching(), + 'Should be dispatching event if forEvent() is called' + ); + } + + public function testGlobalStateCount() + { + $sut = $this->makeInstance(); + + $sut->forEvent(new \stdClass(), $this->makeDispatcher()); + $sut->progress(StateInterface::BEFORE, $this->makeDispatcher()); + + $sut->forEvent(new \stdClass(), $this->makeDispatcher()); + $sut->progress(StateInterface::BEFORE, $this->makeDispatcher()); + + Assert::assertSame( + 2, + $sut->dispatchedEventCount(), + 'Dispatched event count should be 2' + ); + } + + public function testGlobalStateWithCallingForEventAndProgressbeforeAfter() + { + $sut = $this->makeInstance(); + + $sut->forEvent(new \stdClass(), $this->makeDispatcher()); + $sut->progress(StateInterface::BEFORE, $this->makeDispatcher()); + + Assert::assertSame( + 1, + $sut->dispatchedEventCount(), + 'Dispatched event count should be 1 if forEvent() is called' + ); + + Assert::assertSame( + 'stdClass', + $sut->currentEventName(), + 'Current event should be stdClass if forEvent() is called with stdClass' + ); + + Assert::assertTrue( + $sut->isDispatching(), + 'Should be dispatching event' + ); + + $sut->progress(StateInterface::AFTER, $this->makeDispatcher()); + + Assert::assertSame( + 1, + $sut->dispatchedEventCount(), + 'Dispatched event count should be 1 if forEvent() is called' + ); + } +} diff --git a/tests/src/IntegrationTestCase.php b/tests/src/IntegrationTestCase.php new file mode 100644 index 0000000..08ce7bd --- /dev/null +++ b/tests/src/IntegrationTestCase.php @@ -0,0 +1,40 @@ +makeInstance(); + $listenerForEvent = $sut->getListenersForEvent(new \stdClass()); + $this->assertEmpty(\iterator_to_array($listenerForEvent)); + } +} diff --git a/tests/src/PropagationTestTrait.php b/tests/src/PropagationTestTrait.php new file mode 100644 index 0000000..aa06ba8 --- /dev/null +++ b/tests/src/PropagationTestTrait.php @@ -0,0 +1,21 @@ +makeInstance(); + $this->assertFalse($instance->isPropagationStopped()); + } + + public function testStopPropagation(): void + { + $instance = $this->makeInstance(); + $instance->stopPropagation(); + $this->assertTrue($instance->isPropagationStopped()); + } +} diff --git a/tests/src/SubscriberRegisterTestTrait.php b/tests/src/SubscriberRegisterTestTrait.php new file mode 100644 index 0000000..aa5011e --- /dev/null +++ b/tests/src/SubscriberRegisterTestTrait.php @@ -0,0 +1,109 @@ + object callable' => [ + [ + 'event_name' => $obj, + ] + ]; + + $obj = new class { + public function run(): bool + { + return true; + } + }; + + yield 'event_name => object with method run' => [ + [ + 'event_name' => [$obj, 'run'], + ] + ]; + + yield 'event_name => callable' => [ + [ + 'event_name' => function () { + }, + ] + ]; + + yield 'event_name_with_callable_method_from_SubscriberMock' => [ + [ + 'event_name' => 'executeCallable', + 'event_name1' => [ + SubscriberInterface::CALLBACK => 'executeCallable', + SubscriberInterface::PRIORITY => 20, + ], + ] + ]; + + yield 'event_name => callback' => [ + [ + 'event_name' => 'ItalyStrap\Tests\on_callback', + 'event_name1' => 'ItalyStrap\Tests\on_callback', + ] + ]; + + yield 'event_name => [callback|priority]' => [ + [ + 'event_name' => [ + SubscriberInterface::CALLBACK => 'ItalyStrap\Tests\on_callback', + SubscriberInterface::PRIORITY => 20, + ], + 'event_name1' => [ + SubscriberInterface::CALLBACK => 'ItalyStrap\Tests\on_callback', + SubscriberInterface::PRIORITY => 20, + ], + ] + ]; + + yield 'event_name => [callback|priority|args]' => [ + [ + 'event_name' => [ + SubscriberInterface::CALLBACK => 'ItalyStrap\Tests\on_callback', + SubscriberInterface::PRIORITY => 20, + SubscriberInterface::ACCEPTED_ARGS => 6, + ], + 'event_name1' => [ + SubscriberInterface::CALLBACK => 'ItalyStrap\Tests\on_callback', + SubscriberInterface::PRIORITY => 20, + SubscriberInterface::ACCEPTED_ARGS => 6, + ], + ] + ]; + + yield 'event_name => [[callback|priority|args]]' => [ + [ + 'event_name' => [ + [ + SubscriberInterface::CALLBACK => 'ItalyStrap\Tests\on_callback', + SubscriberInterface::PRIORITY => 10, + SubscriberInterface::ACCEPTED_ARGS => 6, + ], + [ + SubscriberInterface::CALLBACK => 'ItalyStrap\Tests\on_callback', + SubscriberInterface::PRIORITY => 20, + SubscriberInterface::ACCEPTED_ARGS => 6, + ], + ], + ] + ]; + } +} diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php new file mode 100644 index 0000000..bfa52de --- /dev/null +++ b/tests/src/UnitTestCase.php @@ -0,0 +1,147 @@ +hooks->reveal(); + } + + protected ObjectProphecy $subscriber; + + protected function makeSubscriber(): SubscriberInterface + { + return $this->subscriber->reveal(); + } + + protected ObjectProphecy $subscriberMock; + + protected function makeSubscriberMock(): SubscriberMock + { + return $this->subscriberMock->reveal(); + } + + protected ObjectProphecy $config; + + protected function makeConfig(): Config + { + return $this->config->reveal(); + } + + + protected ObjectProphecy $fake_injector; + + protected function makeFakeInjector(): Injector + { + return $this->fake_injector->reveal(); + } + + protected ObjectProphecy $subscriberRegister; + + protected function makeSubscriberRegister(): SubscriberRegister + { + return $this->subscriberRegister->reveal(); + } + + protected ObjectProphecy $globalDispatcher; + + protected function makeDispatcher(): EventDispatcherInterface + { + return $this->globalDispatcher->reveal(); + } + + protected ObjectProphecy $listenerRegister; + + protected function makeListenerRegister(): ListenerRegisterInterface + { + return $this->listenerRegister->reveal(); + } + + protected ObjectProphecy $psrDispatcher; + + protected function makePsrDispatcher(): PsrEventDispatcherInterface + { + return $this->psrDispatcher->reveal(); + } + + protected ObjectProphecy $factory; + + /** + * @return CallableFactoryInterface + */ + protected function makeFactory(): CallableFactoryInterface + { + return $this->factory->reveal(); + } + + protected ObjectProphecy $logger; + /** + * @return LoggerInterface + */ + protected function makeLogger(): LoggerInterface + { + return $this->logger->reveal(); + } + + // phpcs:ignore -- Method from Codeception + protected function _before() { + $this->hooks = $this->prophesize(EventDispatcherInterface::class); + $this->globalDispatcher = $this->prophesize(EventDispatcherInterface::class); + $this->listenerRegister = $this->prophesize(ListenerRegisterInterface::class); + $this->subscriber = $this->prophesize(SubscriberInterface::class); + $this->subscriberMock = $this->prophesize(SubscriberMock::class); + + $this->fake_injector = $this->prophesize(Injector::class); + $this->subscriberRegister = $this->prophesize(SubscriberRegister::class); + $this->config = $this->prophesize(Config::class); + + $this->factory = $this->prophesize(CallableFactory::class); + $this->psrDispatcher = $this->prophesize(PsrEventDispatcherInterface::class); + $this->logger = $this->prophesize(LoggerInterface::class); + global $wp_filter; + $wp_filter = []; + } + + // phpcs:ignore -- Method from Codeception + protected function _after() + { + FunctionMockerLe\undefineAll([ + 'do_action', + 'add_filter', + 'remove_filter', + 'apply_filters', + 'current_filter', + 'has_filter', + 'remove_all_filters' + ]); + + global $wp_filter; + $wp_filter = []; + } +} diff --git a/tests/unit/DebugDispatcherTest.php b/tests/unit/DebugDispatcherTest.php new file mode 100644 index 0000000..3148300 --- /dev/null +++ b/tests/unit/DebugDispatcherTest.php @@ -0,0 +1,45 @@ +makePsrDispatcher(), + $this->makeLogger() + ); + $this->assertInstanceOf(EventDispatcherInterface::class, $sut, ''); + return $sut; + } + + /** + * @test + */ + public function itShouldDispatchAndRecordLog() + { + $event = new stdClass(); + + $sut = $this->makeInstance(); + + $this->logger + ->debug(Argument::type('string'), Argument::type('array')) + ->shouldBeCalled(); + + $this->psrDispatcher + ->dispatch(Argument::type('object')) + ->willReturn($event); + + $actual = $sut->dispatch($event); + $this->assertSame($event, $actual, 'It should return the same event'); + } +} diff --git a/tests/unit/DispatcherTest.php b/tests/unit/DispatcherTest.php new file mode 100644 index 0000000..6c659ae --- /dev/null +++ b/tests/unit/DispatcherTest.php @@ -0,0 +1,31 @@ +makeInstance(); + $this->assertSame($event, $sut->dispatch($event)); + } +} diff --git a/tests/unit/EventDispatcherTest.php b/tests/unit/EventDispatcherTest.php deleted file mode 100644 index 498142f..0000000 --- a/tests/unit/EventDispatcherTest.php +++ /dev/null @@ -1,235 +0,0 @@ -assertInstanceOf( EventDispatcherInterface::class, $sut ); - $this->assertInstanceOf( EventDispatcher::class, $sut ); - return $sut; - } - - /** - * @test - */ - public function itShouldbeInstantiable() { - $sut = $this->getInstance(); - } - - /** - * @test - */ - public function itShouldAddListener() { - $sut = $this->getInstance(); - - $args = [ - 'event', - function () { - }, - 10, - 3, - ]; - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('add_filter', function () use (&$calls, $args) { - $calls++; - Assert::assertEquals($args, func_get_args()); - return true; - }); - - $sut->addListener( ...$args ); - - $this->assertEquals( 1, $calls ); - } - - /** - * @test - */ - public function itShouldRemoveListener() { - $sut = $this->getInstance(); - - $args = [ - 'event', - function () { - }, - 10, - 3, - ]; - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('remove_filter', function () use (&$calls, $args) { - $calls++; - Assert::assertEquals($args, func_get_args()); - return true; - }); - - $sut->removeListener( ...$args ); - - $this->assertEquals( 1, $calls ); - } - - public function argumentsProvider() { - return [ - '2 params passed' => [ - [ - 'event_name', - 'arg 1', - ] - ], - '3 params passed' => [ - [ - 'event_name', - 'arg 1', - 'arg 2', - ] - ], - '4 params passed' => [ - [ - 'event_name', - 'arg 1', - 'arg 2', - 'arg 3', - ] - ], - ]; - } - - /** - * @test - * @dataProvider argumentsProvider() - */ - public function itShouldExecuteWith( $args ) { - $sut = $this->getInstance(); - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('do_action', function () use (&$calls, $args) { - $calls++; - Assert::assertEquals($args, func_get_args()); - }); - - $sut->dispatch( ...$args ); - - $this->assertEquals( 1, $calls ); - } - - /** - * @test - * @dataProvider argumentsProvider() - */ - public function itShouldFiltersWith( $args ) { - $sut = $this->getInstance(); - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('apply_filters', function () use (&$calls, $args) { - $calls++; - Assert::assertEquals($args, func_get_args()); - }); - - $sut->filter( ...$args ); - - $this->assertEquals( 1, $calls ); - } - - /** - * @test - */ - public function itShouldReturnCurrentHook() { - $sut = $this->getInstance(); - - $hook_name = 'hook_name'; - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('current_filter', function () use (&$calls, $hook_name) { - $calls++; - return $hook_name; - }); - - $this->assertEquals( $hook_name, $sut->currentEventName(), '' ); - $this->assertEquals( 1, $calls ); - } - - /** - * @test - */ - public function itShouldHasListener() { - $sut = $this->getInstance(); - - $return_val = true; - - $args = [ - 'event', - function () { - }, - ]; - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('has_filter', function () use (&$calls, $return_val, $args) { - $calls++; - Assert::assertEquals($args, func_get_args()); - return $return_val; - }); - - $this->assertEquals( $return_val, $sut->hasListener(...$args), '' ); - $this->assertEquals( 1, $calls ); - } - - /** - * @test - */ - public function itShouldRemoveAllListener() { - $sut = $this->getInstance(); - - $return_val = true; - - $args = [ - 'event', - function () { - }, - ]; - - // phpcs:ignore -- Method from Codeception - FunctionMockerLe\define('remove_all_filters', function ( string $event_name ) use (&$calls, $return_val, $args): bool { - $calls++; - Assert::assertEquals( $args[0], $event_name ); - return $return_val; - }); - - $this->assertEquals( $return_val, $sut->removeAllListener($args[0]), '' ); - $this->assertTrue( $sut->removeAllListener($args[0]), '' ); - $this->assertEquals( 2, $calls ); - } -} diff --git a/tests/unit/EventSubscriptionTest.php b/tests/unit/EventSubscriptionTest.php new file mode 100644 index 0000000..9bf16c3 --- /dev/null +++ b/tests/unit/EventSubscriptionTest.php @@ -0,0 +1,23 @@ +callback, + $this->priority, + $this->acceptedArgs + ); + } +} diff --git a/tests/unit/GlobalDispatcherTest.php b/tests/unit/GlobalDispatcherTest.php new file mode 100644 index 0000000..0995f5b --- /dev/null +++ b/tests/unit/GlobalDispatcherTest.php @@ -0,0 +1,86 @@ +assertInstanceOf(EventDispatcherInterface::class, $sut); + return $sut; + } + + public function argumentsProvider(): iterable + { + return [ + '2 params passed' => [ + [ + 'event_name', + 'arg 1', + ] + ], + '3 params passed' => [ + [ + 'event_name', + 'arg 1', + 'arg 2', + ] + ], + '4 params passed' => [ + [ + 'event_name', + 'arg 1', + 'arg 2', + 'arg 3', + ] + ], + ]; + } + + /** + * @test + * @dataProvider argumentsProvider() + */ + public function itShouldExecuteWith($args) + { + $sut = $this->makeInstance(); + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define('do_action', function () use (&$calls, $args) { + $calls++; + Assert::assertEquals($args, func_get_args()); + }); + + $sut->trigger(...$args); + + $this->assertEquals(1, $calls); + } + + /** + * @test + * @dataProvider argumentsProvider() + */ + public function itShouldFiltersWith($args) + { + $sut = $this->makeInstance(); + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define('apply_filters', function () use (&$calls, $args) { + $calls++; + Assert::assertEquals($args, func_get_args()); + }); + + $sut->filter(...$args); + + $this->assertEquals(1, $calls); + } +} diff --git a/tests/unit/GlobalOrderedListenerProviderTest.php b/tests/unit/GlobalOrderedListenerProviderTest.php new file mode 100644 index 0000000..990a452 --- /dev/null +++ b/tests/unit/GlobalOrderedListenerProviderTest.php @@ -0,0 +1,120 @@ +makeInstance(); + + $args = [ + 'event', + function () { + }, + 10, + 3, + ]; + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define('add_filter', function () use (&$calls, $args) { + $calls++; + Assert::assertEquals($args, func_get_args()); + return true; + }); + + $sut->addListener(...$args); + + $this->assertEquals(1, $calls); + } + + public function testItShouldRemoveListener() + { + $sut = $this->makeInstance(); + + $args = [ + 'event', + function () { + }, + 10, + 3, + ]; + + $calls = 0; + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define( + 'remove_filter', + function ($hook_name, $callback, $priority = 10) use (&$calls) { + $calls++; + return true; + } + ); + + $sut->removeListener(...$args); + + $this->assertEquals(1, $calls); + } + + public function testItShouldHasListener() + { + $sut = $this->makeInstance(); + + $return_val = true; + + $args = [ + 'event', + function () { + }, + ]; + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define('has_filter', function () use (&$calls, $return_val, $args) { + $calls++; + Assert::assertEquals($args, func_get_args()); + return $return_val; + }); + + $this->assertEquals($return_val, $sut->hasListener(...$args), ''); + $this->assertEquals(1, $calls); + } + + public function testItShouldRemoveAllListener() + { + $sut = $this->makeInstance(); + + $return_val = true; + + $args = [ + 'event', + function () { + }, + ]; + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define('remove_all_filters', function ( string $event_name ) use (&$calls, $return_val, $args): bool { + $calls++; + Assert::assertEquals($args[0], $event_name); + return $return_val; + }); + + $this->assertEquals($return_val, $sut->removeAllListener($args[0]), ''); + $this->assertTrue($sut->removeAllListener($args[0]), ''); + $this->assertEquals(2, $calls); + } +} diff --git a/tests/unit/GlobalStateTest.php b/tests/unit/GlobalStateTest.php new file mode 100644 index 0000000..40c2042 --- /dev/null +++ b/tests/unit/GlobalStateTest.php @@ -0,0 +1,36 @@ +makeInstance(); + + $hook_name = 'hook_name'; + + // phpcs:ignore -- Method from Codeception + FunctionMockerLe\define('current_filter', function () use (&$calls, $hook_name) { + $calls++; + return $hook_name; + }); + + $this->assertEquals($hook_name, $sut->currentEventName(), ''); + $this->assertEquals(1, $calls); + } +} diff --git a/tests/unit/PropagationTest.php b/tests/unit/PropagationTest.php new file mode 100644 index 0000000..b428b34 --- /dev/null +++ b/tests/unit/PropagationTest.php @@ -0,0 +1,21 @@ +assertInstanceOf( CallableFactory::class, $sut, '' ); - return $sut; - } - - /** - * @test - */ - public function itShouldBeInstantiable() { - $sut = $this->getInstance(); - } - - /** - * @test - */ - public function itShouldReturn() { - $sut = $this->getInstance(); - } - /** - * @test - */ - public function itShouldBuildCallable() { - $sut = $this->getInstance(); - $sut->buildCallable( function () { - } ); - } +class CallableFactoryTest extends UnitTestCase +{ + private function makeInstance(): CallableFactory + { + return new CallableFactory(); + } + + /** + * @test + */ + public function itShouldBuildCallable() + { + $sut = $this->makeInstance(); + $sut->buildCallable(function () { + }); + } } diff --git a/tests/unit/PsrDispatcher/CallableListenerHolderTest.php b/tests/unit/PsrDispatcher/CallableListenerHolderTest.php index 74f6f8c..945054b 100644 --- a/tests/unit/PsrDispatcher/CallableListenerHolderTest.php +++ b/tests/unit/PsrDispatcher/CallableListenerHolderTest.php @@ -1,140 +1,131 @@ assertInstanceOf( ListenerHolderInterface::class, $sut, '' ); - return $sut; - } - - /** - * @test - */ - public function itShouldBeInstantiable() { - $sut = $this->getInstance( static function ( object $event ) { - } ); - } - - /** - * @test - */ - public function itShouldReturnListener() { - $listener = static function ( object $event ) { - }; - $sut = $this->getInstance( $listener ); - $this->assertSame( $listener, $sut->listener(), '' ); - } - - /** - * @test - */ - public function itShouldReturnVoidListener() { - $event = new \stdClass(); - $event->value = 0; - - $listener = static function ( object $event ) { - $event->value = 42; - }; - $sut = $this->getInstance( $listener ); - $sut->nullListener(); - $sut( $event ); - - $this->assertEmpty( $event->value, '' ); - } - - /** - * @test - */ - public function itShouldExecute() { - $event = $this->prophesize( StoppableEventInterface::class ); - - $event->isPropagationStopped()->willReturn( false ); - - $calls = 0; - $listener = static function ( object $event_obj ) use ( $event, &$calls ) { - Assert::assertSame( $event->reveal(), $event_obj, '' ); - $calls++; - }; - - $sut = $this->getInstance( $listener ); - $sut( $event->reveal() ); - - $this->assertTrue( 1 === $calls, '' ); - } - - /** - * @test - */ - public function itShouldNotExecuteIfEventIsStopped() { - $event = $this->prophesize( StoppableEventInterface::class ); - - $event->isPropagationStopped()->willReturn( true ); - - $calls = 0; - $listener = static function ( object $event_obj ) use ( $event, &$calls ) { - // Never called - $calls++; - }; - - $sut = $this->getInstance( $listener ); - $sut( $event->reveal() ); - - $this->assertTrue( 0 === $calls, '' ); - } - - /** - * @test - */ - public function testSomeCallable() { - $event = new stdClass; - - $listener = new ListenerChangeValueToText(); - $sut = $this->getInstance( [ $listener, 'changeText' ] ); - - $sut( $event ); - - $this->assertTrue( 'new value' === $event->value, '' ); - } - - /** - * @test - */ + +class CallableListenerHolderTest extends UnitTestCase +{ + use ParameterDeriverTrait; + + private function makeInstance(callable $listener): CallableListenerHolder + { + $sut = new CallableListenerHolder($listener); + $this->assertInstanceOf(ListenerHolderInterface::class, $sut, ''); + return $sut; + } + + /** + * @test + */ + public function itShouldBeInstantiable() + { + $sut = $this->makeInstance(static function (object $event) { + }); + } + + /** + * @test + */ + public function itShouldReturnListener() + { + $listener = static function (object $event) { + }; + $sut = $this->makeInstance($listener); + $this->assertSame($listener, $sut->listener(), ''); + } + + /** + * @test + */ + public function itShouldReturnVoidListener() + { + $event = new \stdClass(); + $event->value = 0; + + $listener = static function (object $event) { + $event->value = 42; + }; + $sut = $this->makeInstance($listener); + $sut->nullListener(); + $sut($event); + + $this->assertEmpty($event->value, ''); + } + + /** + * @test + */ + public function itShouldExecute() + { + $event = $this->prophesize(StoppableEventInterface::class); + + $event->isPropagationStopped()->willReturn(false); + + $calls = 0; + $listener = static function (object $event_obj) use ($event, &$calls) { + Assert::assertSame($event->reveal(), $event_obj, ''); + $calls++; + }; + + $sut = $this->makeInstance($listener); + $sut($event->reveal()); + + $this->assertTrue(1 === $calls, ''); + } + + /** + * @test + */ + public function itShouldNotExecuteIfEventIsStopped() + { + $event = $this->prophesize(StoppableEventInterface::class); + + $event->isPropagationStopped()->willReturn(true); + + $calls = 0; + $listener = static function (object $event_obj) use ($event, &$calls) { + // Never called + $calls++; + }; + + $sut = $this->makeInstance($listener); + $sut($event->reveal()); + + $this->assertTrue(0 === $calls, ''); + } + + /** + * @test + */ + public function testSomeCallable() + { + $event = new stdClass(); + + $listener = new ListenerChangeValueToText(); + $sut = $this->makeInstance([ $listener, 'changeText' ]); + + $sut($event); + + $this->assertTrue('new value' === $event->value, ''); + } + + /** + * @test + */ // public function testSomeFeature() // { -// $sut = $this->getInstance( static function ( object $event ) {} ); -// codecept_debug( $type = $this->getParameterType( function ( object $event ) {} ) ); -// $this->assertTrue( $type === 'object' || ( new \ReflectionClass( $type ) )->isInstantiable() ); +// $sut = $this->getInstance( static function ( object $event ) {} ); +// codecept_debug( $type = $this->getParameterType( function ( object $event ) {} ) ); +// $this->assertTrue( $type === 'object' || ( new \ReflectionClass( $type ) )->isInstantiable() ); // } } diff --git a/tests/unit/PsrDispatcher/DebugDispatcherTest.php b/tests/unit/PsrDispatcher/DebugDispatcherTest.php deleted file mode 100644 index 4b34402..0000000 --- a/tests/unit/PsrDispatcher/DebugDispatcherTest.php +++ /dev/null @@ -1,87 +0,0 @@ -dispatcher->reveal(); - } - - /** - * @return LoggerInterface - */ - public function getLogger(): LoggerInterface { - return $this->logger->reveal(); - } - /** - * @var ObjectProphecy - */ - private $logger; - - // phpcs:ignore -- Method from Codeception - protected function _before() { - $this->dispatcher = $this->prophesize( EventDispatcherInterface::class ); - $this->logger = $this->prophesize( LoggerInterface::class ); - } - - // phpcs:ignore -- Method from Codeception - protected function _after() { - } - - /** - * @return DebugDispatcher - */ - private function getInstance() { - $sut = new DebugDispatcher( - $this->getDispatcher(), - $this->getLogger() - ); - $this->assertInstanceOf( EventDispatcherInterface::class, $sut, '' ); - return $sut; - } - - /** - * @test - */ - public function itShouldBeInstantiable() { - $sut = $this->getInstance(); - } - - /** - * @test - */ - public function itShouldDispatchAndRecordLog() { - $sut = $this->getInstance(); - - $this->logger - ->debug( Argument::type('string'), Argument::type('array') ) - ->shouldBeCalled(); - - $this->dispatcher->dispatch( Argument::type('object') )->shouldBeCalled(); - - $sut->dispatch( new stdClass() ); - } -} diff --git a/tests/unit/PsrDispatcher/PsrDispatcherTest.php b/tests/unit/PsrDispatcher/PsrDispatcherTest.php index 14b87f1..1c76817 100644 --- a/tests/unit/PsrDispatcher/PsrDispatcherTest.php +++ b/tests/unit/PsrDispatcher/PsrDispatcherTest.php @@ -1,216 +1,166 @@ dispatcher->reveal(); - } - - /** - * @return CallableFactoryInterface - */ - public function getFactory(): CallableFactoryInterface { - return $this->factory->reveal(); - } - - // phpcs:ignore -- Method from Codeception - protected function _before() { - $this->factory = $this->prophesize( CallableFactory::class ); - $this->dispatcher = $this->prophesize( EventDispatcher::class ); - global $wp_filter; - $wp_filter = []; - } - - // phpcs:ignore -- Method from Codeception - protected function _after() { - global $wp_filter; - $wp_filter = []; - } - - public function getInstance() { - global $wp_filter; - $sut = new PsrDispatcher( $wp_filter, $this->getFactory(), $this->getDispatcher() ); - $this->assertInstanceOf( EventDispatcherInterface::class, $sut, '' ); - return $sut; - } - - /** - * @test - */ - public function itShouldBeInstantiable() { - $sut = $this->getInstance(); - } - - /** - * @test - */ - public function itShouldDispatch() { - $event = new stdClass(); - $expected = [ - 'event_name' => get_class( $event ), - 'event' => $event, - ]; - - $this->dispatcher - ->dispatch( - Argument::type('string'), - Argument::type('object') - ) - ->will(function ( $args ) use ( $expected ) { - Assert::assertSame( $expected['event_name'], $args[0] ); - Assert::assertSame( $expected['event'], $args[1] ); - }) - ->shouldBeCalled(); - - $sut = $this->getInstance(); - $sut->dispatch( $event ); - } - - /** - * @test - */ - public function itShouldAddListener() { - $eventObj = new stdClass(); - $eventName = get_class( $eventObj ); - - $sut = $this->getInstance(); - - $this->factory - ->buildCallable(Argument::type('callable')) - ->will(function ($args) use ($eventObj) { - return static function () use ($eventObj) { - return $eventObj; - }; - }) - ->shouldBeCalled(); - - $this->dispatcher - ->addListener( - Argument::type('string'), - Argument::type('callable'), - Argument::type('integer'), - Argument::type('integer') - ) - ->will(function ($args) use ( $eventName ): bool { - Assert::assertSame( $eventName, $args[0], '' ); - return true; - }) - ->shouldBeCalled(); - - $sut->addListener( $eventName, static function (object $event) { - //No called here - } ); - } - - /** - * @test - */ - public function itShouldRemoveListener() { - - global $wp_filter; - $eventObj = new stdClass(); - $eventName = get_class( $eventObj ); - - $listener = static function (object $event) { - //No called here - }; - - $listener_holder = $this->prophesize( ListenerHolderInterface::class ); - $listener_holder->listener()->willReturn($listener)->shouldBeCalled(); - $listener_holder->nullListener()->shouldBeCalled(); - - $wp_filter[$eventName][10][ uniqid()]['function'] = $listener_holder->reveal(); - - $sut = $this->getInstance(); - - $sut->removeListener( $eventName, $listener ); - } - - /** - * @test - */ - public function itShouldReturnBeforeRemoveListener() { - - global $wp_filter; - $eventObj = new stdClass(); - $eventName = get_class( $eventObj ); - - $listener = static function (object $event) { - //No called here - }; - - $listener_holder = $this->prophesize( ListenerHolderInterface::class ); - $listener_holder->listener()->shouldNotBeCalled(); - - $wp_filter[$eventName][10] = null; - - $sut = $this->getInstance(); - - $this->assertFalse( $sut->removeListener( $eventName, $listener ), '' ); - } - - /** - * @test - */ - public function itShouldThrownErrorOnRemoveListenerIfIsNotListenerHolderInterface() { - - global $wp_filter; - $eventObj = new stdClass(); - $eventName = get_class( $eventObj ); - - $listener = static function (object $event) { - //No called here - }; - - $listener_holder = $this->prophesize( stdClass::class ); - - $wp_filter[$eventName][10][ uniqid()]['function'] = [ - $listener_holder->reveal(), - 'execute' - ]; - - $sut = $this->getInstance(); - - $this->expectException( RuntimeException::class ); - $sut->removeListener( $eventName, $listener ); - } +class PsrDispatcherTest extends UnitTestCase +{ + public function makeInstance(): PsrDispatcher + { + global $wp_filter; + return new PsrDispatcher( + $wp_filter, + $this->makeFactory(), + $this->makeListenerRegister(), + $this->makeDispatcher() + ); + } + + /** + * @test + */ + public function itShouldDispatch() + { + $event = new stdClass(); + $expected = [ + 'event_name' => get_class($event), + 'event' => $event, + ]; + + $this->globalDispatcher + ->trigger( + Argument::type('string'), + Argument::type('object') + ) + ->will(function ($args) use ($expected) { + Assert::assertSame($expected['event_name'], $args[0]); + Assert::assertSame($expected['event'], $args[1]); + }) + ->shouldBeCalled(); + + $sut = $this->makeInstance(); + $result = $sut->dispatch($event); + $this->assertSame($event, $result, 'It should return the same event'); + } + + /** + * @test + */ + public function itShouldAddListener() + { + $eventObj = new stdClass(); + $eventName = get_class($eventObj); + + $sut = $this->makeInstance(); + + $this->factory + ->buildCallable(Argument::type('callable')) + ->will(fn($args) => static fn() => $eventObj) + ->shouldBeCalled(); + + $this->listenerRegister + ->addListener( + Argument::type('string'), + Argument::type('callable'), + Argument::type('integer'), + Argument::type('integer') + ) + ->will(function ($args) use ($eventName): bool { + Assert::assertSame($eventName, $args[0], ''); + return true; + }) + ->shouldBeCalled(); + + $sut->addListener($eventName, static function (object $event) { + //No called here + }); + } + + /** + * @test + */ + public function itShouldRemoveListener() + { + + global $wp_filter; + $eventObj = new stdClass(); + $eventName = get_class($eventObj); + + $listener = static function (object $event) { + //No called here + }; + + $listener_holder = $this->prophesize(ListenerHolderInterface::class); + $listener_holder->listener()->willReturn($listener)->shouldBeCalled(); + $listener_holder->nullListener()->shouldBeCalled(); + + $wp_filter[$eventName][10][ uniqid()]['function'] = $listener_holder->reveal(); + + $sut = $this->makeInstance(); + + $sut->removeListener($eventName, $listener); + } + + /** + * @test + */ + public function itShouldReturnBeforeRemoveListener() + { + + global $wp_filter; + $eventObj = new stdClass(); + $eventName = get_class($eventObj); + + $listener = static function (object $event) { + //No called here + }; + + $listener_holder = $this->prophesize(ListenerHolderInterface::class); + $listener_holder->listener()->shouldNotBeCalled(); + + $wp_filter[$eventName][10] = null; + + $sut = $this->makeInstance(); + + $this->assertFalse($sut->removeListener($eventName, $listener), ''); + } + + /** + * @test + */ + public function itShouldThrownErrorOnRemoveListenerIfIsNotListenerHolderInterface() + { + + global $wp_filter; + $eventObj = new stdClass(); + $eventName = get_class($eventObj); + + $listener = static function (object $event) { + //No called here + }; + + $listener_holder = $this->prophesize(stdClass::class); + + $wp_filter[$eventName][10][ uniqid()]['function'] = [ + $listener_holder->reveal(), + 'execute' + ]; + + $sut = $this->makeInstance(); + + $this->expectException(RuntimeException::class); + $sut->removeListener($eventName, $listener); + } } diff --git a/tests/unit/ResolverExtensionTest.php b/tests/unit/ResolverExtensionTest.php deleted file mode 100644 index 00fd56c..0000000 --- a/tests/unit/ResolverExtensionTest.php +++ /dev/null @@ -1,177 +0,0 @@ -config->reveal(); - } - - /** - * @return SubscriberRegister - */ - public function getEventManager(): SubscriberRegister { - return $this->event_manager->reveal(); - } - - /** - * @return Injector - */ - public function getFakeInjector(): Injector { - return $this->fake_injector->reveal(); - } - - // phpcs:ignore -- Method from Codeception - protected function _before() { - $this->fake_injector = $this->prophesize( Injector::class ); - $this->event_manager = $this->prophesize( SubscriberRegister::class ); - $this->config = $this->prophesize( Config::class ); - } - - // phpcs:ignore -- Method from Codeception - protected function _after() { - } - - protected function getInstance(): SubscribersConfigExtension { - $sut = new SubscribersConfigExtension( $this->getEventManager(), $this->getConfig() ); - $this->assertInstanceOf( Extension::class, $sut, '' ); - $this->assertInstanceOf( SubscribersConfigExtension::class, $sut, '' ); - return $sut; - } - - /** - * @test - */ - public function itShouldBeInstantiable() { - $sut = $this->getInstance(); - } - - /** - * @test - */ - public function itShouldHaveName() { - $sut = $this->getInstance(); - $this->assertStringContainsString( SubscribersConfigExtension::SUBSCRIBERS, $sut->name(), '' ); - } - - /** - * @test - */ - public function callbackShouldSubscribeListenersWithIndexedArray() { - $subscriber = $this->prophesize( Subscriber::class ); - - $this->event_manager->addSubscriber( $subscriber->reveal() )->shouldBeCalled(); - $this->config->get()->shouldNotBeCalled(); - - $this->fake_injector->share( Argument::type('string')) - ->willReturn( $this->getFakeInjector() ) - ->shouldBeCalled(); - - $this->fake_injector->make( Argument::type('string')) - ->willReturn( $subscriber->reveal() ) - ->shouldBeCalled(); - - $sut = $this->getInstance(); - $sut->walk( Subscriber::class, 0, $this->getFakeInjector() ); - } - - /** - * @test - */ - public function callbackShouldSubscribeListenersFormAssociativeArrayWithTrueOptionKey() { - $subscriber = $this->prophesize( Subscriber::class ); - $config = [ - 'key' => true - ]; - $key = \array_keys( $config )[0]; - - $this->event_manager->addSubscriber( $subscriber->reveal() )->shouldBeCalled(); - $this->config->get( $key, false )->willReturn($config[$key])->shouldBeCalled(); - - $this->fake_injector->share( Argument::type('string')) - ->willReturn( $this->getFakeInjector() ) - ->shouldBeCalled(); - - $this->fake_injector->make( Argument::type('string')) - ->willReturn( $subscriber->reveal() ) - ->shouldBeCalled(); - - $sut = $this->getInstance(); - $sut->walk( Subscriber::class, $key, $this->getFakeInjector() ); - } - - /** - * @test - */ - public function callbackShouldNotSubscribeListenersFromAssociativeArrayWithFalseOptionKey() { - $subscriber = $this->prophesize( Subscriber::class ); - $config = [ - 'key' => false - ]; - $key = \array_keys( $config )[0]; - - $this->event_manager->addSubscriber( $subscriber->reveal() )->shouldNotBeCalled(); - $this->config->get( $key, false )->willReturn($config[$key])->shouldBeCalled(); - - $this->fake_injector->share( Argument::type('string')) - ->willReturn( $this->getFakeInjector() ) - ->shouldNotBeCalled(); - - $this->fake_injector->make( Argument::type('string')) - ->willReturn( $subscriber->reveal() ) - ->shouldNotBeCalled(); - - $sut = $this->getInstance(); - $sut->walk( Subscriber::class, $key, $this->getFakeInjector() ); - } - - /** - * @test - */ - public function itShouldExecute() { - $application = $this->prophesize(AurynResolverInterface::class); - - $application->walk(Argument::type('string'), Argument::type('callable'))->shouldBeCalled(); - - $sut = $this->getInstance(); - $sut->execute($application->reveal()); - } -} diff --git a/tests/unit/SubscriberRegisterTest.php b/tests/unit/SubscriberRegisterTest.php index 6225fa7..92eae37 100644 --- a/tests/unit/SubscriberRegisterTest.php +++ b/tests/unit/SubscriberRegisterTest.php @@ -1,379 +1,89 @@ hooks->reveal(); - } - - /** - * @return SubscriberInterface - */ - public function getSubscriber(): SubscriberInterface { - return $this->subscriber->reveal(); - } - - // phpcs:ignore -- Method from Codeception - protected function _before() { - $this->hooks = $this->prophesize( EventDispatcher::class ); - $this->subscriber = $this->prophesize( SubscriberInterface::class ); - } - - // phpcs:ignore -- Method from Codeception - protected function _after() { - } - - private function getInstance() { - $sut = new SubscriberRegister( $this->getHooks() ); - $this->assertInstanceOf( SubscriberRegister::class, $sut, '' ); - return $sut; - } - - /** - * @test - */ - public function instanceOk() { - $sut = $this->getInstance(); - } - - /** - * @test - */ - public function itShouldAddAndRemoveSubscriberFromGenerator() { - $sut = $this->getInstance(); - - $this->subscriber->getSubscribedEvents()->will(function () { - - yield 'event_name' => 'callback'; - - yield 'event_name' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - ]; - - yield 'event_name1' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - ]; - - yield 'event_name' => [ - [ - SubscriberInterface::CALLBACK => 'onCallback', - SubscriberInterface::PRIORITY => 10, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - [ - SubscriberInterface::CALLBACK => 'onCallback', - SubscriberInterface::PRIORITY => 20, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - ]; - - return [ - 'event_name' => 'callback', - 'event_name1' => 'callback', - ]; - }); - - $this->hooks->addListener( - Argument::type( 'string' ), - Argument::type( 'callable' ), - Argument::type( 'int' ), - Argument::type( 'int' ) - )->will(function ( $listener_args ) { - return true; - })->shouldBeCalled(); - - $this->hooks->removeListener( - Argument::type( 'string' ), - Argument::type( 'callable' ), - Argument::type( 'int' ), - Argument::type( 'int' ) - )->will(function ( $listener_args ) { - return true; - })->shouldBeCalled(); - - $sut->addSubscriber( $this->getSubscriber() ); - $sut->removeSubscriber( $this->getSubscriber() ); - } - - public function iteratorProvider() { - - yield 'ArrayObject' => [ - new \ArrayObject(['event_name' => 'callback']), - ]; - - yield 'ConfigObject' => [ - new \ItalyStrap\Config\Config(['event_name' => 'callback']), - ]; - - yield 'ConfigObjectWithAdd' => [ - (new \ItalyStrap\Config\Config())->add( 'event_name', 'callback' ), - ]; - - yield 'ArrayIterator' => [ - new \ArrayIterator([ - 'event_name' => 'callback', - 'event_name1' => 'callback', - ]), - ]; - } - - /** - * @test - * @dataProvider iteratorProvider() - */ - public function itShouldAddAndRemoveSubscriberFromIterators( $iterator ) { - $sut = $this->getInstance(); - - $this->subscriber->getSubscribedEvents()->will(function () use ( $iterator ) { - return $iterator; - }); - - $this->hooks->addListener( - Argument::type( 'string' ), - Argument::type( 'callable' ), - Argument::type( 'int' ), - Argument::type( 'int' ) - )->will(function ( $listener_args ) { - return true; - })->shouldBeCalled(); - - $this->hooks->removeListener( - Argument::type( 'string' ), - Argument::type( 'callable' ), - Argument::type( 'int' ), - Argument::type( 'int' ) - )->will(function ( $listener_args ) { - return true; - })->shouldBeCalled(); - - $sut->addSubscriber( $this->getSubscriber() ); - $sut->removeSubscriber( $this->getSubscriber() ); - } - - public function subscriberProvider() { - return [ - /** - * @TODO Potrebbe essere utile chiamare direttamente - * una callback - */ -// 'event_name => callable' => [ -// [ -// 'event_name' => function () {}, -// 'event_name1' => [ new \stdClass(), 'run' ], -// ] -// ], - 'event_name => callback' => [ - [ - 'event_name' => 'callback', - 'event_name1' => 'callback', - ] - ], - 'event_name => [callback|priority]' => [ - [ - 'event_name' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - ], - 'event_name1' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - ], - ] - ], - 'event_name => [callback|priority|args]' => [ - [ - 'event_name' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - 'event_name1' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - ] - ], - 'event_name => [[callback|priority|args]]' => [ - [ - 'event_name' => [ - [ - SubscriberInterface::CALLBACK => 'onCallback', - SubscriberInterface::PRIORITY => 10, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - [ - SubscriberInterface::CALLBACK => 'onCallback', - SubscriberInterface::PRIORITY => 20, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - ], - ] - ], - ]; - } - - /** - * @test - * @dataProvider subscriberProvider() - * @param $provider_args - */ - public function itShouldAddSubscriberWith( $provider_args ) { - $test = $this; - $sut = $this->getInstance(); - - $this->subscriber->getSubscribedEvents()->willReturn($provider_args); - - $this->hooks->addListener( - Argument::type( 'string' ), - Argument::type( 'callable' ), - Argument::type( 'int' ), - Argument::type( 'int' ) - )->will(function ( $listener_args ) use ( $provider_args, $test ) { - $test->assertArgsPassedAreCorrect( $listener_args, $provider_args ); - return true; - })->shouldBeCalled(); - - $sut->addSubscriber( $this->getSubscriber() ); - } - - /** - * @test - * @dataProvider subscriberProvider() - */ - public function itShouldRemoveSubscriberWith( $provider_args ) { - $test = $this; - $sut = $this->getInstance(); - - $this->subscriber->getSubscribedEvents()->willReturn($provider_args); - - $this->hooks->removeListener( - Argument::type( 'string' ), - Argument::type( 'callable' ), - Argument::type( 'int' ), - Argument::type( 'int' ) - )->will(function ( $listener_args ) use ( $provider_args, $test ) { - $test->assertArgsPassedAreCorrect( $listener_args, $provider_args ); - return true; - })->shouldBeCalled(); - - $sut->removeSubscriber( $this->getSubscriber() ); - } - - /** - * @param $listener_args - * @param $provider_args - */ - private function assertArgsPassedAreCorrect( $listener_args, $provider_args ): void { - - /** - * $args[0] Is the 'event_name' - * $args[1] Is the callback - * $args[2] Is the priority - * $args[3] Is the number of passed arguments - */ - - $event_name = $listener_args[ 0 ]; -// $called_method = \is_callable( $listener_args[ 1 ] ) ? $listener_args[ 1 ] : $listener_args[ 1 ][ 1 ]; - $called_method = $listener_args[ 1 ][ 1 ]; - $priority = $listener_args[ 2 ]; - $accepted_args = $listener_args[ 3 ]; - - Assert::assertArrayHasKey( $event_name, $provider_args, 'Both should be the "event_name"' ); - - if ( isset( $provider_args[ $event_name ][0] ) && is_array( $provider_args[ $event_name ][0] ) ) { - foreach ($provider_args[ $event_name ] as $arg ) { - $this->assertValueFromArrayAreCorrect( - [$event_name => $arg], - $called_method, - $event_name, - $arg[SubscriberInterface::PRIORITY], - $arg[SubscriberInterface::ACCEPTED_ARGS] - ); - } - return; - } - - $this->assertValueFromArrayAreCorrect( $provider_args, $called_method, $event_name, $priority, $accepted_args ); - } - - private function assertValueFromArrayAreCorrect( - $args, - $called_method, - $event_name, - $priority, - $accepted_args - ): void { - Assert::assertEquals( - $called_method, - $args[ $event_name ][ SubscriberInterface::CALLBACK ] ?? $args[ $event_name ], - 'Should be callback name' - ); - - Assert::assertEquals( - $priority, - $args[ $event_name ][ SubscriberInterface::PRIORITY ] ?? 10, // 10 is the default priority - 'Should be default priority' - ); - - Assert::assertEquals( - $accepted_args, - $args[ $event_name ][ SubscriberInterface::ACCEPTED_ARGS ] - ?? 1, // 1 is the defaul number of passed argument - 'Should be default accepted args' - ); - } - - /** - * @test - */ - public function itShouldThrownIfParameterOfSubscriberIsNotValid() { - $test = $this; - $sut = $this->getInstance(); - - $this->subscriber->getSubscribedEvents()->willReturn([ - 'event_name' => [new \stdClass()], - ]); - - $this->hooks->addListener()->shouldNotBeCalled(); - $this->expectException( \RuntimeException::class ); - $sut->addSubscriber( $this->getSubscriber() ); - } +class SubscriberRegisterTest extends UnitTestCase +{ + use SubscriberRegisterTestTrait; + + private function makeInstance(): SubscriberRegister + { + return new SubscriberRegister($this->makeListenerRegister()); + } + + /** + * @dataProvider subscriberProvider() + */ + public function testItShouldAddSubscriberWith($provider_args): void + { + $test = $this; + $sut = $this->makeInstance(); + + $this->subscriberMock + ->executeCallable() + ->willReturn(true); + + $this->subscriberMock + ->getSubscribedEvents() + ->willReturn($provider_args); + + $this->listenerRegister->addListener( + Argument::type('string'), + Argument::type('callable'), + Argument::type('int'), + Argument::type('int') + )->willReturn(true)->shouldBeCalled(); + + $sut->addSubscriber($this->makeSubscriberMock()); + } + + /** + * @dataProvider subscriberProvider() + */ + public function testItShouldRemoveSubscriberWith($provider_args): void + { + $test = $this; + $sut = $this->makeInstance(); + $subscriber = new SubscriberMock($provider_args); + + $this->subscriberMock + ->executeCallable() + ->willReturn(true); + + $this->subscriberMock + ->getSubscribedEvents() + ->willReturn($provider_args); + + $this->listenerRegister->removeListener( + Argument::type('string'), + Argument::type('callable'), + Argument::type('int'), + Argument::type('int') + )->willReturn(true)->shouldBeCalled(); + + $sut->removeSubscriber($this->makeSubscriberMock()); + } + + public function testItShouldThrownIfParameterOfSubscriberIsNotValid(): void + { + $test = $this; + $sut = $this->makeInstance(); + + $this->subscriber->getSubscribedEvents()->willReturn([ + 'event_name' => [new \stdClass()], + ]); + + $this->listenerRegister->addListener()->shouldNotBeCalled(); + + $this->expectException(\RuntimeException::class); + $sut->addSubscriber($this->makeSubscriber()); + } } diff --git a/tests/unit/SubscribersConfigExtensionTest.php b/tests/unit/SubscribersConfigExtensionTest.php new file mode 100644 index 0000000..68022da --- /dev/null +++ b/tests/unit/SubscribersConfigExtensionTest.php @@ -0,0 +1,118 @@ +makeSubscriberRegister(), $this->makeConfig()); + $this->assertInstanceOf(Extension::class, $sut, ''); + return $sut; + } + + /** + * @test + */ + public function itShouldHaveName() + { + $sut = $this->makeInstance(); + $this->assertStringContainsString(SubscribersConfigExtension::SUBSCRIBERS, $sut->name(), ''); + } + + /** + * @test + */ + public function callbackShouldSubscribeListenersWithIndexedArray() + { + $subscriber = $this->prophesize(SubscriberMock::class); + + $this->subscriberRegister->addSubscriber($subscriber->reveal())->shouldBeCalled(); + $this->config->get()->shouldNotBeCalled(); + + $this->fake_injector->share(Argument::type('string')) + ->willReturn($this->makeFakeInjector()) + ->shouldBeCalled(); + + $this->fake_injector->make(Argument::type('string')) + ->willReturn($subscriber->reveal()) + ->shouldBeCalled(); + + $sut = $this->makeInstance(); + $sut(SubscriberMock::class, 0, $this->makeFakeInjector()); + } + + /** + * @test + */ + public function callbackShouldSubscribeListenersFormAssociativeArrayWithTrueOptionKey() + { + $subscriber = $this->prophesize(SubscriberMock::class); + $config = [ + 'key' => true + ]; + $key = \array_keys($config)[0]; + + $this->subscriberRegister->addSubscriber($subscriber->reveal())->shouldBeCalled(); + $this->config->get($key, false)->willReturn($config[$key])->shouldBeCalled(); + + $this->fake_injector->share(Argument::type('string')) + ->willReturn($this->makeFakeInjector()) + ->shouldBeCalled(); + + $this->fake_injector->make(Argument::type('string')) + ->willReturn($subscriber->reveal()) + ->shouldBeCalled(); + + $sut = $this->makeInstance(); + $sut(SubscriberMock::class, $key, $this->makeFakeInjector()); + } + + /** + * @test + */ + public function callbackShouldNotSubscribeListenersFromAssociativeArrayWithFalseOptionKey() + { + $subscriber = $this->prophesize(SubscriberMock::class); + $config = [ + 'key' => false + ]; + $key = \array_keys($config)[0]; + + $this->subscriberRegister->addSubscriber($subscriber->reveal())->shouldNotBeCalled(); + $this->config->get($key, false)->willReturn($config[$key])->shouldBeCalled(); + + $this->fake_injector->share(Argument::type('string')) + ->willReturn($this->makeFakeInjector()) + ->shouldNotBeCalled(); + + $this->fake_injector->make(Argument::type('string')) + ->willReturn($subscriber->reveal()) + ->shouldNotBeCalled(); + + $sut = $this->makeInstance(); + $sut(SubscriberMock::class, $key, $this->makeFakeInjector()); + } + + /** + * @test + */ + public function itShouldExecute() + { + $application = $this->prophesize(AurynResolverInterface::class); + + $application->walk(Argument::type('string'), Argument::type('callable'))->shouldBeCalled(); + + $sut = $this->makeInstance(); + $sut->execute($application->reveal()); + } +} diff --git a/tests/wpunit.suite.yml b/tests/wpunit.suite.yml deleted file mode 100644 index 060cdcf..0000000 --- a/tests/wpunit.suite.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for unit or integration tests that require WordPress functions and classes. - -actor: WpunitTester -modules: - enabled: - - WPLoader - - \Helper\Wpunit - config: - WPLoader: - wpRootFolder: "%WP_ROOT_FOLDER%" - dbName: "%TEST_DB_NAME%" - dbHost: "%TEST_DB_HOST%" - dbUser: "%TEST_DB_USER%" - dbPassword: "%TEST_DB_PASSWORD%" - tablePrefix: "%TEST_TABLE_PREFIX%" - domain: "%TEST_SITE_WP_DOMAIN%" - adminEmail: "%TEST_SITE_ADMIN_EMAIL%" - title: "Event" - plugins: ['event/index.php'] - activatePlugins: ['event/index.php'] \ No newline at end of file diff --git a/tests/wpunit/IntegrationTest.php b/tests/wpunit/IntegrationTest.php deleted file mode 100644 index 601b72a..0000000 --- a/tests/wpunit/IntegrationTest.php +++ /dev/null @@ -1,225 +0,0 @@ -dispatcher = new EventDispatcher(); - $this->register = new SubscriberRegister( $this->dispatcher ); - - // Your set up methods here. - } - - public function tearDown(): void { - // Your tear down methods here. - - global $wp_filter; - $wp_filter = []; - // Then... - parent::tearDown(); - } - - /** - * @test - */ - public function itShouldOutputTextOnEventName() { - - $this->dispatcher->addListener( 'event_name', function () { - echo 'Value printed'; - } ); - - $this->expectOutputString( 'Value printed' ); - $this->dispatcher->dispatch( 'event_name' ); - } - - /** - * @test - */ - public function testClassWithDispatchDependency() { - $some_class = new ClassWithDispatchDependency( $this->dispatcher ); - - $this->dispatcher->addListener( - ClassWithDispatchDependency::EVENT_NAME, - function ( string $value ) { - return 'New value'; - } - ); - - $some_class->filterValue(); - - $this->assertStringContainsString('New value', $some_class->value(), ''); - } - - /** - * @test - */ - public function subscriberShouldEchoTextWhenEventIsExecuted() { - - $subscriber = new class implements SubscriberInterface { - - /** - * @inheritDoc - */ - public function getSubscribedEvents(): array { - return [ - 'event_name' => 'method', - 'other_event_name' => [ - [ - SubscriberInterface::CALLBACK => 'onCallback', - SubscriberInterface::PRIORITY => 20, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - [ - SubscriberInterface::CALLBACK => 'onCallback', - SubscriberInterface::PRIORITY => 10, - SubscriberInterface::ACCEPTED_ARGS => 6, - ], - ], - ]; - } - - public function method() { - echo 'Value printed'; - } - - public function onCallback( string $filtered ) { - return $filtered . ' Value printed'; - } - }; - - $this->register->addSubscriber( $subscriber ); - - $this->expectOutputString( 'Value printed' ); - $this->dispatcher->dispatch( 'event_name' ); - - $filtered = (string) $this->dispatcher->filter( 'other_event_name', '' ); - $this->assertStringContainsString( 'Value printed Value printed', $filtered, '' ); - } - - /** - * @test - */ - public function itShouldPrintText() { - - $injector = new Injector(); - $injector->share($injector); - - $injector->alias(EventDispatcherInterface::class, EventDispatcher::class); - $injector->share( EventDispatcherInterface::class ); - $injector->share( SubscriberRegister::class ); - $event_resolver = $injector->make( SubscribersConfigExtension::class, [ - ':config' => ConfigFactory::make([ - Subscriber::class => false - ]), - ] ); - - $dependencies = ConfigFactory::make([ -// AurynResolver::ALIASES => [ -// HooksInterface::class => Hooks::class, -// ], -// AurynResolver::SHARING => [ -// HooksInterface::class, -// EventManager::class, -// ], - SubscribersConfigExtension::SUBSCRIBERS => [ - Subscriber::class, -// Subscriber::class => false, - ], - ]); - -// $empress = new AurynResolver( $injector, $dependencies ); - $empress = $injector->make( AurynResolver::class, [ - ':dependencies' => $dependencies - ] ); - $empress->extend( $event_resolver ); - $empress->resolve(); - - $this->expectOutputString( 'Some text' ); - ( $injector->make( EventDispatcher::class ) )->dispatch( 'event' ); - } - - private function configExample() { - - $test = [ - 'hook_name => callback' => [ - [ - 'hook_name' => 'callback' - ] - ], - 'hook_name => [callback|priority]' => [ - [ - 'hook_name' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - ] - ] - ], - 'hook_name => [callback|priority|args]' => [ - [ - 'hook_name' => [ - SubscriberInterface::CALLBACK => 'callback', - SubscriberInterface::PRIORITY => 20, - SubscriberInterface::ACCEPTED_ARGS => 6, - ] - ] - ], - ]; - - $config = [ - 'subscribers' => [ - Subscriber::class, - ], - 'listeners' => [ - Listener::class => [ - 'event_name' => '', - 'method' => '', - 'priority' => '', - 'args' => '', - ] - ], - ]; - } -} diff --git a/tests/wpunit/Psr14IntegrationTest.php b/tests/wpunit/Psr14IntegrationTest.php deleted file mode 100644 index baa6695..0000000 --- a/tests/wpunit/Psr14IntegrationTest.php +++ /dev/null @@ -1,235 +0,0 @@ -listener = new class implements ListenerProviderInterface { - - /** - * @inheritDoc - */ - public function getListenersForEvent( object $event ): iterable { - // TODO: Implement getListenersForEvent() method. - } - }; - // Your set up methods here. - } - - public function tearDown(): void { - // Your tear down methods here. - - global $wp_filter, $wp_actions; - unset($wp_filter, $wp_actions); - // Then... - parent::tearDown(); - } - - /** - * @test - */ - public function itShouldAddFunctionListenerAndChangeValue() { - - $sut = $this->getEventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - - /** @var object $event */ - $event = $sut->dispatch( new EventFirst() ); - - $this->assertEquals( 42, $event->value, '' ); - $this->assertFalse( $event->isPropagationStopped(), '' ); - } - - /** - * @test - */ - public function itShouldRemoveFunctionListenerAndReturnValueWithoutChanges() { - - $sut = $this->getEventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - $sut->removeListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - - /** @var object $event */ - $event = $sut->dispatch( new EventFirst() ); - - $this->assertEquals( 0, $event->value, '' ); - $this->assertFalse( $event->isPropagationStopped(), '' ); - } - - /** - * @test - */ - public function itShouldStopPropagation() { - - $sut = $this->getEventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - $sut->addListener( EventFirst::class, [new ListenerChangeValueToText, 'changeText' ]); - - // Here it will set value to false and stop propagation - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_false_and_stop_propagation' ); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_77' ); - - - $event = new EventFirst(); - - /** @var object $event */ - $sut->dispatch( $event ); - - $this->assertEquals( false, $event->value, '' ); - $this->assertTrue( $event->isPropagationStopped(), '' ); - } - - /** - * @test - */ - public function itShouldNotStopPropagation() { - - $sut = $this->getEventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - $sut->addListener( EventFirst::class, [new ListenerChangeValueToText, 'changeText' ]); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_77' ); - - - $event = new EventFirst(); - - /** @var object $event */ - $sut->dispatch( $event ); - - $this->assertEquals( 77, $event->value, '' ); - $this->assertFalse( $event->isPropagationStopped(), '' ); - } - - /** - * @test - */ - public function itShouldRemoveListenerAndReturnValue77() { - - $sut = $this->getEventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - $sut->addListener( EventFirst::class, [new ListenerChangeValueToText, 'changeText' ]); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_false_and_stop_propagation' ); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_77' ); - - $sut->removeListener( - EventFirst::class, - __NAMESPACE__ . '\listener_change_value_to_false_and_stop_propagation' - ); - - $event = new EventFirst(); - - /** @var object $event */ - $sut->dispatch( $event ); - - $this->assertEquals( 77, $event->value, '' ); - $this->assertFalse( $event->isPropagationStopped(), '' ); - } - - /** - * @test - */ - public function ifSameEventIsDispatchedMoreThanOnceItShouldStopPropagationIfListenerStopPropagation() { - - $sut = $this->getEventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - $sut->addListener( EventFirst::class, [new ListenerChangeValueToText, 'changeText' ]); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_false_and_stop_propagation' ); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_77' ); - - $event = new EventFirst(); - - /** @var object $event */ - $event = $sut->dispatch( $event ); - - $this->assertEquals( false, $event->value, '' ); - $this->assertTrue( $event->isPropagationStopped(), '' ); - - $event = $sut->dispatch( new EventFirst() ); - - $this->assertEquals( false, $event->value, '' ); - $this->assertTrue( $event->isPropagationStopped(), '' ); - } - - /** - * - */ - public function ifSameEventIsDispatchedMoreThanOnceItShouldStopPropagationIfListenerStopPropagationSymfonyMirror() { - $sut = new EventDispatcher(); - - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_42' ); - $sut->addListener( EventFirst::class, [new ListenerChangeValueToText, 'changeText' ]); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_false_and_stop_propagation' ); - $sut->addListener( EventFirst::class, __NAMESPACE__ . '\listener_change_value_to_77' ); - - $event = new EventFirst(); - - /** @var object $event */ - $event = $sut->dispatch( $event ); - - $this->assertEquals( false, $event->value, '' ); - $this->assertTrue( $event->isPropagationStopped(), '' ); - - $event = $sut->dispatch( new EventFirst() ); - - $this->assertEquals( false, $event->value, '' ); - $this->assertTrue( $event->isPropagationStopped(), '' ); - } - - public function testServerRequest() { - codecept_debug($_SERVER['REQUEST_TIME']); - codecept_debug( \json_encode( \is_int( $_SERVER['REQUEST_TIME'] ) ) ); - } -}