diff --git a/.gitattributes b/.gitattributes index 70fe279..b577be6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ /bin export-ignore /tests export-ignore +/.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore @@ -9,3 +10,5 @@ /phpunit.xml export-ignore /README.md export-ignore /readme.txt export-ignore +/ecs.php export-ignore +/rector.php export-ignore \ No newline at end of file diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..d77c91c --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,40 @@ +name: "Setup PHP & Composer" +description: "Setup PHP & installs composer dependencies" +inputs: + PHP_VERSION: + description: PHP version + default: "8.2" + required: false + type: string + PHP_TOOLS: + description: PHP version + default: "" + required: false + type: string + COMPOSER_ARGS: + description: Set of arguments passed to Composer. + default: "--prefer-dist --no-scripts" + required: false + type: string + INSTALL_DEPS: + description: Whether to install dependencies or not. + default: true + required: false + type: boolean + +runs: + using: composite + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.PHP_VERSION }} + tools: ${{ format('composer,{0}', inputs.PHP_TOOLS) }} + - name: Remove lock file + shell: bash + run: rm -f composer.lock + - name: Install Composer dependencies + if: ${{ inputs.INSTALL_DEPS == 'true' }} + uses: ramsey/composer-install@v3 + with: + composer-options: ${{ inputs.COMPOSER_ARGS }} diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml new file mode 100644 index 0000000..760e775 --- /dev/null +++ b/.github/workflows/php-lint.yml @@ -0,0 +1,46 @@ +name: PHP lint + +on: + push: + branches: + - master + paths: + - "**.php" + - "**composer.json" + pull_request: + branches: + - master + paths: + - "**.php" + - "**composer.json" + types: + - opened + - synchronize + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + php-lint: + name: PHP lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install dependencies + uses: ./.github/actions/setup + with: + PHP_TOOLS: "parallel-lint,cs2pr" + INSTALL_DEPS: false + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v47 + with: + files: "**.php" + + - name: Run PHP lint + if: steps.changed-files.outputs.all_changed_files != '' + run: parallel-lint --exclude .git --exclude vendor --checkstyle ${{ join(steps.changed-files.outputs.all_changed_files, ' ') }} | cs2pr diff --git a/.github/workflows/php-rector.yml b/.github/workflows/php-rector.yml new file mode 100644 index 0000000..d87e02a --- /dev/null +++ b/.github/workflows/php-rector.yml @@ -0,0 +1,41 @@ +name: PHP Rector + +on: + push: + branches: + - master + paths: + - "**.php" + - "**composer.json" + pull_request: + branches: + - master + paths: + - "**.php" + - "**composer.json" + types: + - opened + - synchronize + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + php-cs: + name: PHP Rector + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install dependencies + uses: ./.github/actions/setup + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v47 + + - name: Run Rector checks + if: steps.changed-files.outputs.all_changed_files != '' + run: ./vendor/bin/rector process --dry-run ${{ steps.changed-files.outputs.files }} diff --git a/.github/workflows/php-unit-tests.yml b/.github/workflows/php-unit-tests.yml new file mode 100644 index 0000000..56ec4f4 --- /dev/null +++ b/.github/workflows/php-unit-tests.yml @@ -0,0 +1,114 @@ +name: PHP unit tests + +on: + push: + branches: + - master + paths: + - "**workflows/php-unit-tests.yml" + - "**.php" + - "**composer.*" + - "**phpunit.xml" + pull_request: + branches: + - master + paths: + - "**workflows/php-unit-tests.yml" + - "**.php" + - "**composer.*" + - "**phpunit.xml" + types: + - opened + - synchronize + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ============================================================================= + # Detect PHP versions from composer.json + # ============================================================================= + php-versions: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.versions.outputs.version }} + steps: + - uses: actions/checkout@v6 + - id: versions + uses: WyriHaximus/github-action-composer-php-versions-in-range@v1 + with: + upcomingReleases: true + + # ============================================================================= + # Compatibility tests: PHP versions x WP versions + # ============================================================================= + php-unit-tests: + needs: php-versions + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + php: ${{ fromJson(needs.php-versions.outputs.versions) }} + wp: ["latest"] + dependency-version: ["highest", "lowest"] + experimental: [false] + coverage: [false] + include: + # Oldest supported WP with lowest deps + - php: "8.2" + wp: "6.3" + dependency-version: "lowest" + experimental: false + coverage: false + # Coverage: PHP 8.2, WP latest, highest deps + - php: "8.2" + wp: "latest" + dependency-version: "highest" + experimental: false + coverage: true + # Trunk (experimental) + - php: "8.5" + wp: "trunk" + dependency-version: "highest" + experimental: true + coverage: false + + name: PHP ${{ matrix.php }} | WP ${{ matrix.wp }} | ${{ matrix.dependency-version }}${{ matrix.coverage && ' | coverage' || '' }} + + steps: + - uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: ${{ matrix.coverage && 'pcov' || 'none' }} + tools: composer:v2 + extensions: curl, date, dom, iconv, json, libxml, gd, sqlite3 + ini-values: ${{ matrix.coverage && 'pcov.directory=src' || '' }} + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - uses: ramsey/composer-install@v4 + with: + dependency-versions: ${{ matrix.dependency-version }} + + # PHP 8.5: Collision and curl have deprecations we can't fix + - name: Disable failOnDeprecation for PHP 8.5 + if: matrix.experimental + run: sed -i 's/failOnDeprecation="true"/failOnDeprecation="false"/' phpunit.xml + + - name: Create coverage directory + if: matrix.coverage + run: mkdir -p build/logs + + - name: Run default testsuite + run: composer test ${{ matrix.coverage && '-- --coverage-php build/coverage-default.cov' || '' }} + env: + WP_VERSION: ${{ matrix.wp }} diff --git a/.gitignore b/.gitignore index aa40f5b..4760fb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,25 @@ -# osx noise +!/src/Cache +.tmp +*~ + +# OS .DS_Store -profile -# xcode noise -build/* -*.mode1 -*.mode1v3 -*.mode2v3 -*.perspective -*.perspectivev3 -*.pbxuser -*.xcworkspace -xcuserdata +# IDE and local environment +.idea +.vscode +*.swp -# svn & cvs -.svn -CVS +# Packages vendor +composer.lock +# Application +build +/cache +twig-cache/* + +# Tests +.phpunit.result.cache +/wp-content +/tmp +/.phpunit.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8c0b968..0000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -sudo: false - -dist: xenial - -services: mysql - -language: php - -php: - - 5.6.40 - - 7.4 - -env: - - WP_VERSION=latest WP_MULTISITE=0 - - WP_VERSION=latest WP_MULTISITE=1 - -before_script: - - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION - - composer install --dev - -script: - - mkdir -p build/logs - - php vendor/bin/phpunit --coverage-clover build/logs/clover.xml - -after_script: - - php vendor/bin/coveralls -v - -after_success: - - coveralls diff --git a/README.md b/README.md index 501f0f9..0db8c11 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Routes -Simple routing for WordPress. Designed for usage with [Timber](https://github.com/timber/timber) -[![Build Status](https://img.shields.io/travis/Upstatement/routes/master.svg?style=flat-square)](https://travis-ci.org/Upstatement/routes) -[![Coverage Status](https://img.shields.io/coveralls/Upstatement/routes.svg?style=flat-square)](https://coveralls.io/r/Upstatement/routes?branch=master) -[![Packagist Downloads](https://img.shields.io/packagist/dt/Upstatement/routes.svg?style=flat-square)]() +Simple routing for WordPress. Designed for usage with [Timber](https://github.com/timber/timber) +[![PHP unit tests](https://github.com/Upstatement/routes/actions/workflows/php-unit-tests.yml/badge.svg?branch=2.x)](https://github.com/Upstatement/routes/actions/workflows/php-unit-tests.yml?query=branch:2.x) +[![Latest Stable Version](https://img.shields.io/packagist/v/Upstatement/routes.svg?style=flat-square)](https://packagist.org/packages/Upstatement/routes) ### Basic Usage + ```php /* functions.php */ Routes::map('myfoo/bar', 'my_callback_function'); @@ -21,6 +21,7 @@ Routes::map('my-events/:event', function($params) { Using routes makes it easy for you to implement custom pagination — and anything else you might imagine in your wildest dreams of URLs and parameters. OMG so easy! ## Some examples + In your functions.php file, this can be called anywhere (don't hook it to init or another action or it might be called too late) ```php @@ -77,12 +78,13 @@ my-users/:userid/edit A function that should fire when the pattern matches the request. Callback takes one argument which is an array of the parameters passed in the URL. So in this example: `'info/:name/page/:pg'`, $params would have data for: -* `$data['name']` -* `$data['pg']` + +- `$data['name']` +- `$data['pg']` ... which you can use in the callback function as a part of your query -* * * +--- ## load diff --git a/Routes.php b/Routes.php index 32e6732..aaed4e0 100755 --- a/Routes.php +++ b/Routes.php @@ -1,166 +1,233 @@ router)) { + $route = $upstatement_routes->router->match(); - function __construct(){ - add_action('init', array($this, 'match_current_request') ); - add_action('wp_loaded', array($this, 'match_current_request') ); - } + unset($upstatement_routes->router); - static function match_current_request() { - global $upstatement_routes; - if (isset($upstatement_routes->router)) { - $route = $upstatement_routes->router->match(); - - unset($upstatement_routes->router); - - if ($route && isset($route['target'])) { - if ( isset($route['params']) ) { - call_user_func($route['target'], $route['params']); - } else { - call_user_func($route['target']); - } - } - } - } + if ($route && isset($route['target'])) { + if (isset($route['params'])) { + call_user_func($route['target'], $route['params']); + } else { + call_user_func($route['target']); + } + } + } + } - /** - * @param string $route A string to match (ex: 'myfoo') - * @param callable $callback A function to run, examples: - * Routes::map('myfoo', 'my_callback_function'); - * Routes::map('mybaq', array($my_class, 'method')); - * Routes::map('myqux', function() { - * //stuff goes here - * }); - */ - public static function map($route, $callback, $name = '') { - global $upstatement_routes; - if (!isset($upstatement_routes->router)) { - $upstatement_routes->router = new AltoRouter(); - $site_url = get_bloginfo('url'); - $site_url_parts = explode('/', $site_url); - $site_url_parts = array_slice($site_url_parts, 3); - $base_path = implode('/', $site_url_parts); - if (!$base_path || strpos($route, $base_path) === 0) { - $base_path = '/'; - } else { - $base_path = '/' . $base_path . '/'; - } - // Clean any double slashes that have resulted - $base_path = str_replace( "//", "/", $base_path ); - $upstatement_routes->router->setBasePath($base_path); - } - $route = self::convert_route($route); - $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', trailingslashit($route), $callback, $name); - $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', untrailingslashit($route), $callback, $name); - } + /** + * Maps a route to a callback function. + * + * @api + * @param string $route A string to match (ex: 'myfoo'). + * @param callable $callback A callback function to call when the route is matched. + * This can be a string for a function name, + * an array for a class method, or an anonymous function. + * @param string $name An optional name for the route, which can be used to generate URLs with the url() method. + * @return void + * + * @example + * ```php + * Routes::map('myfoo', 'my_callback_function'); + * Routes::map('mybaq', array($my_class, 'method')); + * Routes::map('myqux', function() { + * //stuff goes here + * }); + * ``` + */ + public static function map($route, $callback, $name = '') + { + global $upstatement_routes; + if (!isset($upstatement_routes->router)) { + $upstatement_routes->router = new AltoRouter(); + $site_url = get_bloginfo('url'); + $site_url_parts = explode('/', $site_url); + $site_url_parts = array_slice($site_url_parts, 3); + $base_path = implode('/', $site_url_parts); + if (!$base_path || str_starts_with($route, $base_path)) { + $base_path = '/'; + } else { + $base_path = '/' . $base_path . '/'; + } + // Clean any double slashes that have resulted + $base_path = str_replace('//', '/', $base_path); + $upstatement_routes->router->setBasePath($base_path); + } + $route = self::convert_route($route); + $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', trailingslashit($route), $callback, $name); + $upstatement_routes->router->map('GET|POST|PUT|DELETE|HEAD', untrailingslashit($route), $callback, $name); + } - /** - * @return string A string in a format for AltoRouter - * ex: [:my_param] - */ - public static function convert_route($route_string) { - if (strpos($route_string, '[') > -1) { - return $route_string; - } - $route_string = preg_replace('/(:)\w+/', '/[$0]', $route_string); - $route_string = str_replace('[[', '[', $route_string); - $route_string = str_replace(']]', ']', $route_string); - $route_string = str_replace('[/:', '[:', $route_string); - $route_string = str_replace('//[', '/[', $route_string); - if ( strpos($route_string, '/') === 0 ) { - $route_string = substr($route_string, 1); - } - return $route_string; - } + /** + * + * Used internally to convert a route string with :param style parameters + * to the format used by AltoRouter, which is [:param]. + * If the route string already contains [ and ] characters, + * it is assumed to be in the correct format and is returned unchanged. + * + * @internal + * @param string $route_string A route string with :param style parameters (ex: 'myfoo/:my_param'). + * @return string A string in a format for AltoRouter + * ex: [:my_param] + */ + public static function convert_route($route_string) + { + if (str_contains($route_string, '[')) { + return $route_string; + } + $route_string = preg_replace('/(:)\w+/', '/[$0]', $route_string); + $route_string = str_replace('[[', '[', $route_string); + $route_string = str_replace(']]', ']', $route_string); + $route_string = str_replace('[/:', '[:', $route_string); + $route_string = str_replace('//[', '/[', $route_string); + if (str_starts_with($route_string, '/')) { + $route_string = substr($route_string, 1); + } + return $route_string; + } - /** - * @param string $template A php file to load (ex: 'single.php') - * @param array|bool $tparams An array of data to send to the php file. Inside the php file - * this data can be accessed via: - * global $params; - * @param int $status_code A code for the status (ex: 200) - * @param WP_Query $query Use a WP_Query object in the template file instead of - * the default query - * @param int $priority The priority used by the "template_include" filter - * @return bool - */ - public static function load($template, $tparams = false, $query = false, $status_code = 200, $priority = 10) { - $fullPath = is_readable($template); - if (!$fullPath) { - $template = locate_template($template); - } - if ($tparams){ - global $params; - $params = $tparams; - } - if ($status_code) { - add_filter('status_header', function($status_header, $header, $text, $protocol) use ($status_code) { - $text = get_status_header_desc($status_code); - $header_string = "$protocol $status_code $text"; - return $header_string; - }, 10, 4 ); - if (404 != $status_code) { - add_action('parse_query', function($query) { - if ($query->is_main_query()){ - $query->is_404 = false; - } - },1); - add_action('template_redirect', function(){ - global $wp_query; - $wp_query->is_404 = false; - },1); - } - } + /** + * Loads a template file and sends data to it. This is used in the callback functions for the routes defined with the map() method, + * to load a specific template file when a route is matched, and to send data to that template file. + * + * @api + * @param string $template A php file to load (ex: 'single.php'). + * @param array|bool $tparams An array of data to send to the php file. Inside the php file this data can be accessed via: `global $params;`. + * @param WP_Query|callable|array|string $query A WP_Query object, a callable that returns a WP_Query object, an array of query vars, or a query string. This will be used to set the main query for the request, which can be accessed with the global $wp_query variable in the template file. If a callable is passed, it will be called at the time of the 'parse_request' action, and should return a WP_Query object. + * @param int $status_code A code for the status (ex: 200). + * @param int $priority The priority used by the "template_include" filter. + * @return bool + */ + public static function load($template, $tparams = false, $query = false, $status_code = 200, $priority = 10) + { + $full_path = is_readable($template); + if (!$full_path) { + $template = locate_template($template); + } + if ($tparams) { + global $params; + $params = $tparams; + } + if ($status_code) { + add_filter( + 'status_header', + function ($status_header, $header, $text, $protocol) use ($status_code) { + $text = get_status_header_desc($status_code); + $header_string = "$protocol $status_code $text"; + return $header_string; + }, + 10, + 4 + ); + if (404 !== $status_code) { + add_action( + 'parse_query', + function ($query) { + if ($query->is_main_query()) { + $query->is_404 = false; + } + }, + 1 + ); + add_action( + 'template_redirect', + function () { + global $wp_query; + $wp_query->is_404 = false; + }, + 1 + ); + } + } - if ($query) { - add_action('parse_request', function() use ($query) { - global $wp; - if ( is_callable($query) ) - $query = call_user_func($query); + if ($query) { + add_action( + 'parse_request', + function () use ($query) { + global $wp; + if (is_callable($query)) { + $query = call_user_func($query); + } - if ( is_array($query) ) - $wp->query_vars = $query; - elseif ( !empty($query) ) - parse_str($query, $wp->query_vars); - else - return true; // Could not interpret query. Let WP try. + if (is_array($query)) { + $wp->query_vars = $query; + } elseif (!empty($query)) { + parse_str((string) $query, $wp->query_vars); + } else { + return true; // Could not interpret query. Let WP try. + } - return false; - }); - } - if ($template) { - add_filter('template_include', function($t) use ($template) { - return $template; - }, $priority); - return true; - } - return false; - } + return false; + } + ); + } + if ($template) { + add_filter( + 'template_include', + fn($current_template) => $template, + $priority + ); + return true; + } + return false; + } } global $upstatement_routes; $upstatement_routes = new Routes(); -if ( file_exists($composer_autoload = __DIR__ . '/vendor/autoload.php') - || file_exists($composer_autoload = WP_CONTENT_DIR.'/vendor/autoload.php')){ - require_once($composer_autoload); +if ( + file_exists($composer_autoload = __DIR__ . '/vendor/autoload.php') + || file_exists($composer_autoload = WP_CONTENT_DIR . '/vendor/autoload.php') +) { + require_once $composer_autoload; } - diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh deleted file mode 100644 index ba3a0eb..0000000 --- a/bin/install-wp-tests.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash - -if [ $# -lt 3 ]; then - echo "usage: $0 [db-host] [wp-version]" - exit 1 -fi - -DB_NAME=$1 -DB_USER=$2 -DB_PASS=$3 -DB_HOST=${4-localhost} -WP_VERSION=${5-latest} - -WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} -WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} - -echo $WP_TESTS_DIR; - -download() { - if [ `which curl` ]; then - curl -s "$1" > "$2"; - elif [ `which wget` ]; then - wget -nv -O "$2" "$1" - fi -} - -if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then - WP_TESTS_TAG="tags/$WP_VERSION" -else - # http serves a single offer, whereas https serves multiple. we only want one - download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json - grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json - LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') - if [[ -z "$LATEST_VERSION" ]]; then - echo "Latest WordPress version could not be found" - exit 1 - fi - WP_TESTS_TAG="tags/$LATEST_VERSION" -fi - -set -ex - -install_wp() { - - if [ -d $WP_CORE_DIR ]; then - return; - fi - - mkdir -p $WP_CORE_DIR - - if [ $WP_VERSION == 'latest' ]; then - local ARCHIVE_NAME='latest' - else - local ARCHIVE_NAME="wordpress-$WP_VERSION" - fi - - download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz - tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR - - download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php -} - -install_test_suite() { - # portable in-place argument for both GNU sed and Mac OSX sed - if [[ $(uname -s) == 'Darwin' ]]; then - local ioption='-i .bak' - else - local ioption='-i' - fi - - # set up testing suite if it doesn't yet exist - if [ ! -d $WP_TESTS_DIR ]; then - # set up testing suite - mkdir -p $WP_TESTS_DIR - svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes - fi - - cd $WP_TESTS_DIR - - if [ ! -f wp-tests-config.php ]; then - download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php - fi - -} - -install_db() { - # parse DB_HOST for port or socket references - local PARTS=(${DB_HOST//\:/ }) - local DB_HOSTNAME=${PARTS[0]}; - local DB_SOCK_OR_PORT=${PARTS[1]}; - local EXTRA="" - - if ! [ -z $DB_HOSTNAME ] ; then - if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then - EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" - elif ! [ -z $DB_SOCK_OR_PORT ] ; then - EXTRA=" --socket=$DB_SOCK_OR_PORT" - elif ! [ -z $DB_HOSTNAME ] ; then - EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" - fi - fi - - # create database - mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA -} - -install_wp -install_test_suite -install_db \ No newline at end of file diff --git a/composer.json b/composer.json index a248eed..1dfa774 100755 --- a/composer.json +++ b/composer.json @@ -1,39 +1,72 @@ { - "name": "upstatement/routes", - "description": "Manage rewrites and routes in WordPress with this dead-simple plugin", - "keywords": [ - "routes", - "routing", - "rewrite", - "redirects" - ], - "homepage": "https://www.upstatement.com", - "license": "MIT", - "authors": [ - { - "name": "Jared Novack", - "email": "jared@upstatement.com", - "homepage": "https://www.upstatement.com" - } - ], - "support": { - "issues": "https://github.com/Upstatement/routes/issues", - "wiki": "https://github.com/Upstatement/routes/wiki", - "source": "https://github.com/Upstatement/routes" - }, - "require": { - "php": ">=7.3", - "altorouter/altorouter": "^2.0.2", - "composer/installers": "^1.0 || ^2.0" - }, - "require-dev": { - "phpunit/phpunit": "5.7.16", - "wp-cli/wp-cli": "*", - "satooshi/php-coveralls": "*" - }, - "autoload": { - "psr-0": { - "Routes": "" - } + "name": "upstatement/routes", + "description": "Manage rewrites and routes in WordPress with this dead-simple plugin", + "keywords": [ + "routes", + "routing", + "rewrite", + "redirects" + ], + "homepage": "https://www.upstatement.com", + "license": "MIT", + "authors": [ + { + "name": "Jared Novack", + "email": "jared@upstatement.com", + "homepage": "https://www.upstatement.com" } + ], + "support": { + "issues": "https://github.com/Upstatement/routes/issues", + "wiki": "https://github.com/Upstatement/routes/wiki", + "source": "https://github.com/Upstatement/routes" + }, + "require": { + "php": ">=8.2", + "altorouter/altorouter": "^2.0.2", + "composer/installers": "^2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5 || ^12", + "wp-cli/wp-cli": "*", + "rector/rector": "^2.0", + "mantle-framework/cache": "^1.16", + "mantle-framework/config": "^1.16", + "mantle-framework/container": "^1.16", + "mantle-framework/contracts": "^1.16", + "mantle-framework/database": "^1.16", + "mantle-framework/events": "^1.16", + "mantle-framework/faker": "^1.15", + "mantle-framework/filesystem": "^1.16", + "mantle-framework/framework-views": "^1.16", + "mantle-framework/http": "^1.16", + "mantle-framework/http-client": "^1.16", + "mantle-framework/support": "^1.16", + "mantle-framework/testing": "^1.16", + "mantle-framework/testkit": "^1.16", + "mantle-framework/view": "^1.16", + "php-parallel-lint/php-parallel-lint": "^1.3", + "squizlabs/php_codesniffer": "^3.0", + "symplify/easy-coding-standard": "^13" + }, + "scripts": { + "cs": "ecs check", + "cs:fix": "ecs check --fix", + "test": "phpunit", + "lint": "parallel-lint --exclude .git --exclude vendor .", + "rector": "rector process --dry-run", + "rector:fix": "rector process" + }, + "autoload": { + "psr-0": { + "Routes": "" + } + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "alleyinteractive/composer-wordpress-autoloader": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..580acfd --- /dev/null +++ b/ecs.php @@ -0,0 +1,64 @@ +withPaths( [__DIR__ . '/Routes.php'] ) + ->withSkip( + [ + NotOperatorWithSuccessorSpaceFixer::class, + ] + ) + /** + * Import + * + * @see https://cs.symfony.com/doc/rules/index.html#import + */ + ->withRules( + [ + NoLeadingImportSlashFixer::class, + SingleImportPerStatementFixer::class, + ] + ) + ->withConfiguredRule( + FullyQualifiedStrictTypesFixer::class, + [ + 'import_symbols' => true, + ] + ) + ->withConfiguredRule( + GlobalNamespaceImportFixer::class, + [ + 'import_classes' => true, + ] + ) + /** + * NativeFunctionInvocation + * + * @see https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html + */ + ->withConfiguredRule( + NativeFunctionInvocationFixer::class, + [ + 'include' => [ + '@all', + ], + 'scope' => 'namespaced', + 'strict' => true, + ] + ) + ->withSets( + [ + SetList::PSR_12, + SetList::ARRAY, + SetList::SPACES, + SetList::NAMESPACES, + ] + ); diff --git a/phpunit.xml b/phpunit.xml index c9e1d5a..dedf6c8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,19 +1,23 @@ + - - - ./tests/ - - - - - Routes.php - - - + + + ./tests/ + + + + + ./Routes.php + + + \ No newline at end of file diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..1686c75 --- /dev/null +++ b/rector.php @@ -0,0 +1,30 @@ +withPaths( + [ + __DIR__ . '/Routes.php', + ] + ) + ->withPhpSets( + php82: true, + ) + ->withSets( + [ + PHPUnitSetList::PHPUNIT_100, + PHPUnitSetList::PHPUNIT_110, + ] + ) + ->withSkip( + [ + ArrayToFirstClassCallableRector::class, + StringableForToStringRector::class, + ] + ); diff --git a/tests/RoutesTest.php b/tests/RoutesTest.php new file mode 100644 index 0000000..b552cae --- /dev/null +++ b/tests/RoutesTest.php @@ -0,0 +1,319 @@ +assertTrue($template); + } + + function testThemeRouteDoesntExist() + { + $template = Routes::load('singlefoo.php'); + $this->assertFalse($template); + } + + function testFullPathRoute() + { + $hello = WP_CONTENT_DIR . '/plugins/hello.php'; + $template = Routes::load($hello); + $this->assertTrue($template); + } + + function testFullPathRouteDoesntExist() + { + $hello = WP_CONTENT_DIR . '/plugins/hello-foo.php'; + $template = Routes::load($hello); + $this->assertFalse($template); + } + + function testRouterClass() + { + $this->assertTrue(class_exists('AltoRouter')); + } + + function testAppliedRoute() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'foo', + function () use ($phpunit) { + global $matches; + $matches = []; + $phpunit->assertTrue(true); + $matches[] = true; + } + ); + $this->get(home_url('foo')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testRouteWithVariable() + { + $post_name = 'ziggy'; + $post = $this->factory->post->create( + [ + 'post_title' => 'Ziggy', + 'post_name' => $post_name, + ] + ); + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'mything/:slug', + function ($params) use ($phpunit) { + global $matches; + $matches = []; + if ('ziggy' == $params['slug']) { + $matches[] = true; + } + } + ); + $this->get(home_url('/mything/' . $post_name)); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testRouteWithAltoVariable() + { + $post_name = 'ziggy'; + $post = $this->factory->post->create( + [ + 'post_title' => 'Ziggy', + 'post_name' => $post_name, + ] + ); + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'mything/[*:slug]', + function ($params) use ($phpunit) { + global $matches; + $matches = []; + if ('ziggy' == $params['slug']) { + $matches[] = true; + } + } + ); + $this->get(home_url('/mything/' . $post_name)); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testRouteWithMultiArguments() + { + $phpunit = $this; + Routes::map( + 'artist/[:artist]/song/[:song]', + function ($params) use ($phpunit) { + global $matches; + $matches = []; + if ($params['artist'] == 'smashing-pumpkins') { + $matches[] = true; + } + if ($params['song'] == 'mayonaise') { + $matches[] = true; + } + } + ); + $this->get(home_url('/artist/smashing-pumpkins/song/mayonaise')); + $this->matchRoutes(); + global $matches; + $this->assertEquals(2, count($matches)); + } + + function testRouteWithMultiArgumentsOldStyle() + { + $phpunit = $this; + global $matches; + Routes::map( + 'studio/:studio/movie/:movie', + function ($params) use ($phpunit) { + global $matches; + $matches = []; + if ($params['studio'] == 'universal') { + $matches[] = true; + } + if ($params['movie'] == 'brazil') { + $matches[] = true; + } + } + ); + $this->get(home_url('/studio/universal/movie/brazil/')); + $this->matchRoutes(); + $this->assertEquals(2, count($matches)); + } + + function testRouteAgainstPostName() + { + $post_name = 'jared'; + $post = $this->factory->post->create( + [ + 'post_title' => 'Jared', + 'post_name' => $post_name, + ] + ); + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'randomthing/' . $post_name, + function () use ($phpunit) { + global $matches; + $matches = []; + $phpunit->assertTrue(true); + $matches[] = true; + } + ); + $this->get(home_url('/randomthing/' . $post_name)); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testVerySimpleRoute() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'crackers', + function () use ($phpunit) { + global $matches; + $matches = []; + $matches[] = true; + } + ); + $this->get(home_url('crackers')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testVerySimpleRouteTrailingSlash() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'bip/', + function () use ($phpunit) { + global $matches; + $matches = []; + $matches[] = true; + } + ); + $this->get(home_url('bip')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testVerySimpleRouteTrailingSlashInRequest() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'bopp', + function () use ($phpunit) { + global $matches; + $matches = []; + $matches[] = true; + } + ); + $this->get(home_url('bopp/')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + + function testVerySimpleRouteTrailingSlashInRequestAndMapping() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'zappers', + function () use ($phpunit) { + global $matches; + $matches = []; + $matches[] = true; + } + ); + $this->get(home_url('zappers/')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testVerySimpleRoutePrecedingSlash() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + '/gobbles', + function () use ($phpunit) { + global $matches; + $matches = []; + $matches[] = true; + } + ); + $this->get(home_url('gobbles')); + $this->matchRoutes(); + $this->assertEquals(1, count($matches)); + } + + function testFailedRoute() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + global $matches; + $matches = []; + $phpunit = $this; + Routes::map( + 'foo', + function () use ($phpunit) { + $matches = []; + $phpunit->assertTrue(false); + $matches[] = true; + } + ); + $this->get(home_url('bar')); + $this->matchRoutes(); + $this->assertEquals(0, count($matches)); + } + + function testRouteWithClassCallback() + { + Routes::map('classroute', ['RoutesTest', '_testCallback']); + $this->get(home_url('classroute')); + $this->matchRoutes(); + global $matches_class_test; + $this->assertEquals(1, count($matches_class_test)); + } + + function matchRoutes() + { + global $upstatement_routes; + $upstatement_routes->match_current_request(); + } + + static function _testCallback() + { + global $matches_class_test; + $matches_class_test = []; + $matches_class_test[] = true; + } +} diff --git a/tests/Supports/single.php b/tests/Supports/single.php new file mode 100644 index 0000000..36dd01f --- /dev/null +++ b/tests/Supports/single.php @@ -0,0 +1,3 @@ +with_multisite($isMultisite); -require $_tests_dir . '/includes/bootstrap.php'; +$manager + ->with_sqlite() + ->loaded(function () {}) + ->install(); diff --git a/tests/single.php b/tests/single.php deleted file mode 100644 index a814366..0000000 --- a/tests/single.php +++ /dev/null @@ -1 +0,0 @@ -assertTrue($template); - } - - function testThemeRouteDoesntExist(){ - $template = Routes::load('singlefoo.php'); - $this->assertFalse($template); - } - - function testFullPathRoute(){ - $hello = WP_CONTENT_DIR.'/plugins/hello.php'; - $template = Routes::load($hello); - $this->assertTrue($template); - } - - function testFullPathRouteDoesntExist(){ - $hello = WP_CONTENT_DIR.'/plugins/hello-foo.php'; - $template = Routes::load($hello); - $this->assertFalse($template); - } - - function testRouterClass(){ - $this->assertTrue(class_exists('AltoRouter')); - } - - function testAppliedRoute(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('foo', function() use ($phpunit) { - global $matches; - $matches = array(); - $phpunit->assertTrue(true); - $matches[] = true; - }); - $this->go_to(home_url('foo')); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testRouteWithVariable() { - $post_name = 'ziggy'; - $post = $this->factory->post->create(array('post_title' => 'Ziggy', 'post_name' => $post_name)); - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('mything/:slug', function($params) use ($phpunit) { - global $matches; - $matches = array(); - if ('ziggy' == $params['slug']) { - $matches[] = true; - } - }); - $this->go_to(home_url('/mything/'.$post_name)); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testRouteWithAltoVariable() { - $post_name = 'ziggy'; - $post = $this->factory->post->create(array('post_title' => 'Ziggy', 'post_name' => $post_name)); - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('mything/[*:slug]', function($params) use ($phpunit) { - global $matches; - $matches = array(); - if ('ziggy' == $params['slug']) { - $matches[] = true; - } - }); - $this->go_to(home_url('/mything/'.$post_name)); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testRouteWithMultiArguments() { - $phpunit = $this; - Routes::map('artist/[:artist]/song/[:song]', function($params) use ($phpunit) { - global $matches; - $matches = array(); - if ($params['artist'] == 'smashing-pumpkins') { - $matches[] = true; - } - if ($params['song'] == 'mayonaise') { - $matches[] = true; - } - }); - $this->go_to(home_url('/artist/smashing-pumpkins/song/mayonaise')); - $this->matchRoutes(); - global $matches; - $this->assertEquals(2, count($matches)); - } - - function testRouteWithMultiArgumentsOldStyle() { - $phpunit = $this; - global $matches; - Routes::map('studio/:studio/movie/:movie', function($params) use ($phpunit) { - global $matches; - $matches = array(); - if ($params['studio'] == 'universal') { - $matches[] = true; - } - if ($params['movie'] == 'brazil') { - $matches[] = true; - } - }); - $this->go_to(home_url('/studio/universal/movie/brazil/')); - $this->matchRoutes(); - $this->assertEquals(2, count($matches)); - } - - function testRouteAgainstPostName(){ - $post_name = 'jared'; - $post = $this->factory->post->create(array('post_title' => 'Jared', 'post_name' => $post_name)); - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('randomthing/'.$post_name, function() use ($phpunit) { - global $matches; - $matches = array(); - $phpunit->assertTrue(true); - $matches[] = true; - }); - $this->go_to(home_url('/randomthing/'.$post_name)); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testVerySimpleRoute(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('crackers', function() use ($phpunit) { - global $matches; - $matches = array(); - $matches[] = true; - }); - $this->go_to(home_url('crackers')); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testVerySimpleRouteTrailingSlash(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('bip/', function() use ($phpunit) { - global $matches; - $matches = array(); - $matches[] = true; - }); - $this->go_to(home_url('bip')); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testVerySimpleRouteTrailingSlashInRequest(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('bopp', function() use ($phpunit) { - global $matches; - $matches = array(); - $matches[] = true; - }); - $this->go_to(home_url('bopp/')); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - - function testVerySimpleRouteTrailingSlashInRequestAndMapping(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('zappers', function() use ($phpunit) { - global $matches; - $matches = array(); - $matches[] = true; - }); - $this->go_to(home_url('zappers/')); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testVerySimpleRoutePreceedingSlash(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('/gobbles', function() use ($phpunit) { - global $matches; - $matches = array(); - $matches[] = true; - }); - $this->go_to(home_url('gobbles')); - $this->matchRoutes(); - $this->assertEquals(1, count($matches)); - } - - function testFailedRoute(){ - $_SERVER['REQUEST_METHOD'] = 'GET'; - global $matches; - $matches = array(); - $phpunit = $this; - Routes::map('foo', function() use ($phpunit){ - $matches = array(); - $phpunit->assertTrue(false); - $matches[] = true; - }); - $this->go_to(home_url('bar')); - $this->matchRoutes(); - $this->assertEquals(0, count($matches)); - } - - function testRouteWithClassCallback() { - Routes::map('classroute', array('TestRoutes', '_testCallback')); - $this->go_to(home_url('classroute')); - $this->matchRoutes(); - global $matches; - $this->assertEquals(1, count($matches)); - } - - function matchRoutes() { - global $upstatement_routes; - $upstatement_routes->match_current_request(); - } - - static function _testCallback() { - global $matches; - $matches[] = true; - } -}