diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d190130f40..d25ecf973b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: - - '5.x' - - '5.next' - '6.x' pull_request: branches: @@ -21,10 +19,8 @@ jobs: fail-fast: false matrix: include: - - php-version: '8.2' - dependencies: 'lowest' - php-version: '8.5' - dependencies: 'highest' + dependencies: 'lowest' - php-version: '8.5' dependencies: 'highest' @@ -63,7 +59,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.5' extensions: mbstring, intl coverage: none tools: cs2pr, phpstan:2.3 diff --git a/.htaccess b/.htaccess index 54b08e82e1..4e2d24528d 100644 --- a/.htaccess +++ b/.htaccess @@ -7,6 +7,6 @@ RewriteEngine on RewriteRule ^(\.well-known/.*)$ $1 [L] - RewriteRule ^$ webroot/ [L] - RewriteRule (.*) webroot/$1 [L] + RewriteRule ^$ public/ [L] + RewriteRule (.*) public/$1 [L] diff --git a/README.md b/README.md index 5a614fa263..499aa499f6 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,37 @@ # CakePHP Application Skeleton -![Build Status](https://github.com/cakephp/app/actions/workflows/ci.yml/badge.svg?branch=5.x) +![Build Status](https://github.com/cakephp/app/actions/workflows/ci.yml/badge.svg?branch=6.x) [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/app.svg?style=flat-square)](https://packagist.org/packages/cakephp/app) [![PHPStan](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg?style=flat-square)](https://github.com/phpstan/phpstan) -A skeleton for creating applications with [CakePHP](https://cakephp.org) 5.x. +A skeleton for creating applications with [CakePHP](https://cakephp.org) 6.x, requiring PHP 8.5 or higher. + +This branch tracks CakePHP 6 development and allows development dependencies while preferring stable packages. +Migrations is tracked on `6.x-dev`. Installation and attribute routing currently depend on +[Migrations 6 compatibility](https://github.com/cakephp/migrations/pull/1121) and the +[repeated route attribute fix](https://github.com/cakephp/cakephp/pull/19663). The framework source code can be found here: [cakephp/cakephp](https://github.com/cakephp/cakephp). ## Installation 1. Download [Composer](https://getcomposer.org/doc/00-intro.md) or update `composer self-update`. -2. Run `php composer.phar create-project --prefer-dist cakephp/app [app_name]`. +2. Run `php composer.phar create-project --prefer-dist cakephp/app myapp "6.x-dev"`. If Composer is installed globally, run ```bash -composer create-project --prefer-dist cakephp/app +composer create-project --prefer-dist cakephp/app myapp "6.x-dev" ``` In case you want to use a custom app dir name (e.g. `/myapp/`): ```bash -composer create-project --prefer-dist cakephp/app myapp +composer create-project --prefer-dist cakephp/app /myapp "6.x-dev" ``` -You can now either use your machine's webserver to view the default home page, or start -up the built-in webserver with: +Configure your webserver's document root to the application's `public/` directory, +which contains the front controller and static assets. You can also start the built-in webserver with: ```bash bin/cake server -p 8765 @@ -36,7 +41,7 @@ Then visit `http://localhost:8765` to see the welcome page. ## Demo app -Check out the [5.x-demo branch](https://github.com/cakephp/app/tree/5.x-demo), which contains demo migrations and a seeder. +The [5.x-demo branch](https://github.com/cakephp/app/tree/5.x-demo) is a CakePHP 5 example containing demo migrations and a seeder. See the [README](https://github.com/cakephp/app/blob/5.x-demo/README.md) on how to get it running. ## Update @@ -51,6 +56,27 @@ Read and edit the environment specific `config/app_local.php` and set up the `'Datasources'` and any other configuration relevant for your application. Other environment agnostic settings can be changed in `config/app.php`. +The test suite uses PHPUnit 13. + +## Routing + +Routes are declared explicitly using [PHP attributes](https://book.cakephp.org/6.x/development/attribute-routing.html) +on controller actions. The home page and `/pages/*` use `#[Get]` attributes on `PagesController::display()`. +Catchall routes are not enabled: add attributes to new controllers, including controllers generated by Bake, +or connect explicit routes in `config/routes.php` or `Application::routes()`. +Attribute routes are bootstrapped in `Application::routes()`. The included `config/routes.php` +contains a commented example of conventional routing and is loaded before the attribute routes. +You can delete this file if you only use attribute routing. +Controllers inherit `DashedRoute` from the `#[RouteClass]` attribute on `AppController`. + +The attribute resolver scans application controllers, including nested controller directories. +Attribute metadata uses the `_cake_attributes_` PHP cache in production and is not persistently cached in debug mode. +Clear the metadata cache after deploying controller or route changes: + +```bash +bin/cake cache clear _cake_attributes_ +``` + ## Layout The app skeleton uses [Milligram](https://milligram.io/) (v1.3) minimalist CSS diff --git a/composer.json b/composer.json index 9ec38086ab..86437a459d 100644 --- a/composer.json +++ b/composer.json @@ -5,18 +5,18 @@ "type": "project", "homepage": "https://cakephp.org", "require": { - "php": ">=8.2", - "cakephp/cakephp": "5.4.*", - "cakephp/migrations": "^5.0", + "php": ">=8.5", + "cakephp/cakephp": "^6.0", + "cakephp/migrations": "6.x-dev", "cakephp/plugin-installer": "^2.0", "mobiledetect/mobiledetectlib": "^4.8.03" }, "require-dev": { - "cakephp/bake": "^3.6", - "cakephp/cakephp-codesniffer": "^5.3", - "cakephp/debug_kit": "^5.2", + "cakephp/bake": "^4.0", + "cakephp/cakephp-codesniffer": "^6.0", + "cakephp/debug_kit": "^6.0", "josegonzalez/dotenv": "^4.0", - "phpunit/phpunit": "^11.5.3 || ^12.1.3 || ^13.0" + "phpunit/phpunit": "^13.0" }, "suggest": { "cakephp/repl": "Console tools for a REPL interface for CakePHP applications.", @@ -53,5 +53,7 @@ "cs-check": "phpcs --colors -p", "cs-fix": "phpcbf --colors -p", "test": "phpunit --colors=always" - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/config/app.php b/config/app.php index 9876cf421c..9ab30f5b39 100644 --- a/config/app.php +++ b/config/app.php @@ -1,6 +1,7 @@ env('APP_DEFAULT_TIMEZONE', 'UTC'), 'base' => false, 'dir' => 'src', - 'webroot' => 'webroot', + 'webroot' => 'public', 'wwwRoot' => WWW_ROOT, //'baseUrl' => env('SCRIPT_NAME'), 'fullBaseUrl' => env('APP_FULL_BASE_URL', false), @@ -133,6 +134,42 @@ 'duration' => '+1 years', 'url' => env('CACHE_CAKEMODEL_URL', null), ], + + // Attribute metadata is cached indefinitely in production. Clear at deploy time. + '_cake_attributes_' => [ + 'className' => PhpEngine::class, + 'prefix' => 'myapp_attributes_', + 'path' => CACHE . 'attributes' . DS, + 'duration' => 0, + ], + ], + + /* + * Configure attribute discovery and metadata caching. + * The default configuration scans application controllers and is used by attribute routing. + * + * Options: + * + * - `paths` - string[] - File glob patterns relative to `basePath`; `**` scans recursively. + * - `basePath` - string - Absolute base directory. Defaults to APP. + * - `excludePaths` - string[] - Glob patterns to skip. Defaults to []. + * - `excludeAttributes` - string[] - Fully qualified attribute classes to ignore. Defaults to []. + * - `cache` - string|false - Cache configuration name, or false to disable caching. + * - `validateFiles` - bool - Re-scan when cached files change. Defaults to false. + * Enable for development when caching is used; leave disabled in production + * to avoid file modification checks on every request. + * + * bootstrap.php disables caching when debug = true. In production, use PhpEngine + * and refresh metadata during deployment with `bin/cake attributes warm`. + * + * See https://book.cakephp.org/6.x/core-libraries/attribute-resolver.html + */ + 'AttributeResolver' => [ + 'default' => [ + 'paths' => ['Controller/*Controller.php', 'Controller/**/*Controller.php'], + 'basePath' => APP, + 'cache' => '_cake_attributes_', + ], ], /* diff --git a/config/bootstrap.php b/config/bootstrap.php index 82a92c60fa..26b5762164 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -32,6 +32,7 @@ */ require CORE_PATH . 'config' . DS . 'bootstrap.php'; +use Cake\AttributeResolver\AttributeResolver; use Cake\Cache\Cache; use Cake\Core\Configure; use Cake\Core\Configure\Engine\PhpConfig; @@ -102,6 +103,7 @@ if (Configure::read('debug')) { Configure::write('Cache._cake_model_.duration', '+1 minute'); Configure::write('Cache._cake_translations_.duration', '+1 minute'); + Configure::write('AttributeResolver.default.cache', false); } /* @@ -184,6 +186,7 @@ * This will also remove the loaded config data from memory. */ Cache::setConfig(Configure::consume('Cache')); +AttributeResolver::setConfig(Configure::consume('AttributeResolver')); ConnectionManager::setConfig(Configure::consume('Datasources')); TransportFactory::setConfig(Configure::consume('EmailTransport')); Mailer::setConfig(Configure::consume('Email')); @@ -210,7 +213,7 @@ * You can enable default locale format parsing by adding calls * to `useLocaleParser()`. This enables the automatic conversion of * locale specific date formats when processing request data. For details see - * @link https://book.cakephp.org/5/en/core-libraries/internationalization-and-localization.html#parsing-localized-datetime-data + * @link https://book.cakephp.org/6.x/core-libraries/internationalization-and-localization.html#parsing-localized-datetime-data */ // \Cake\Database\TypeFactory::build('time')->useLocaleParser(); // \Cake\Database\TypeFactory::build('date')->useLocaleParser(); @@ -231,7 +234,7 @@ // \Cake\Utility\Inflector::rules('uninflected', ['dontinflectme']); // set a custom date and time format -// see https://book.cakephp.org/5/en/core-libraries/time.html#setting-the-default-locale-and-format-string +// see https://book.cakephp.org/6.x/core-libraries/time.html#setting-the-default-locale-and-format-string // and https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax // \Cake\I18n\Date::setToStringFormat('dd.MM.yyyy'); // \Cake\I18n\Time::setToStringFormat('dd.MM.yyyy HH:mm'); diff --git a/config/paths.php b/config/paths.php index 37a24819d7..4afde80949 100644 --- a/config/paths.php +++ b/config/paths.php @@ -47,13 +47,13 @@ define('CONFIG', ROOT . DS . 'config' . DS); /* - * File path to the webroot directory. + * File path to the public directory. * - * To derive your webroot from your webserver change this to: + * To derive this path from your webserver change this to: * * `define('WWW_ROOT', rtrim($_SERVER['DOCUMENT_ROOT'], DS) . DS);` */ -define('WWW_ROOT', ROOT . DS . 'webroot' . DS); +define('WWW_ROOT', ROOT . DS . 'public' . DS); /* * Path to the tests directory. diff --git a/config/plugins.php b/config/plugins.php index 6be3095e01..28de4ef15c 100644 --- a/config/plugins.php +++ b/config/plugins.php @@ -4,7 +4,7 @@ * * In this file, you configure which plugins are loaded in the different states your app can be. * It's loaded via the `parent::bootstrap();` call inside your `Application::bootstrap()` method. - * For more information see https://book.cakephp.org/5/en/plugins.html#loading-plugins-via-configuration-array + * For more information see https://book.cakephp.org/6.x/plugins.html#loading-plugins-via-configuration-array * * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) diff --git a/config/routes.php b/config/routes.php index 7a8d2b7aef..f21b570df8 100644 --- a/config/routes.php +++ b/config/routes.php @@ -2,13 +2,6 @@ /** * Routes configuration. * - * In this file, you set up routes to your controllers and their actions. - * Routes are very important mechanism that allows you to freely connect - * different URLs to chosen controllers and their actions (functions). - * - * It's loaded within the context of `Application::routes()` method which - * receives a `RouteBuilder` instance `$routes` as method argument. - * * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * @@ -21,76 +14,10 @@ * @license https://opensource.org/licenses/mit-license.php MIT License */ -use Cake\Routing\Route\DashedRoute; use Cake\Routing\RouteBuilder; -/* - * This file is loaded in the context of the `Application` class. - * So you can use `$this` to reference the application class instance - * if required. - */ -return function (RouteBuilder $routes): void { - /* - * The default class to use for all routes - * - * The following route classes are supplied with CakePHP and are appropriate - * to set as the default: - * - * - Route - * - InflectedRoute - * - DashedRoute - * - * If no call is made to `Router::defaultRouteClass()`, the class used is - * `Route` (`Cake\Routing\Route\Route`) - * - * Note that `Route` does not do any inflections on URLs which will result in - * inconsistently cased URLs when used with `{plugin}`, `{controller}` and - * `{action}` markers. - */ - $routes->setRouteClass(DashedRoute::class); - - $routes->scope('/', function (RouteBuilder $builder): void { - /* - * Here, we are connecting '/' (base path) to a controller called 'Pages', - * its action called 'display', and we pass a param to select the view file - * to use (in this case, templates/Pages/home.php)... - */ - $builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']); - - /* - * ...and connect the rest of 'Pages' controller's URLs. - */ - $builder->connect('/pages/*', 'Pages::display'); - - /* - * Connect catchall routes for all controllers. - * - * The `fallbacks` method is a shortcut for - * - * ``` - * $builder->connect('/{controller}', ['action' => 'index']); - * $builder->connect('/{controller}/{action}/*', []); - * ``` - * - * It is NOT recommended to use fallback routes after your initial prototyping phase! - * See https://book.cakephp.org/5/en/development/routing.html#fallbacks-method for more information - */ - $builder->fallbacks(); - }); - - /* - * If you need a different set of middleware or none at all, - * open new scope and define routes there. - * - * ``` - * $routes->scope('/api', function (RouteBuilder $builder): void { - * // No $builder->applyMiddleware() here. - * - * // Parse specified extensions from URLs - * // $builder->setExtensions(['json', 'xml']); - * - * // Connect API actions here. - * }); - * ``` - */ +return static function (RouteBuilder $routes): void { + // Attribute routes are connected in Application::routes(). + // You can also connect explicit routes here, for example: + // $routes->get('/contact', ['controller' => 'Pages', 'action' => 'display', 'contact'], 'contact'); }; diff --git a/index.php b/index.php index 4591769163..71e4484aec 100644 --- a/index.php +++ b/index.php @@ -13,4 +13,4 @@ * @license https://opensource.org/licenses/mit-license.php MIT License */ -require 'webroot' . DIRECTORY_SEPARATOR . 'index.php'; +require 'public' . DIRECTORY_SEPARATOR . 'index.php'; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 38c7d781be..bed23120af 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,7 +5,7 @@ stopOnFailure="false" bootstrap="tests/bootstrap.php" cacheDirectory=".phpunit.cache" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"> + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/13.0/phpunit.xsd"> diff --git a/webroot/.htaccess b/public/.htaccess similarity index 100% rename from webroot/.htaccess rename to public/.htaccess diff --git a/webroot/css/cake.css b/public/css/cake.css similarity index 100% rename from webroot/css/cake.css rename to public/css/cake.css diff --git a/webroot/css/fonts.css b/public/css/fonts.css similarity index 100% rename from webroot/css/fonts.css rename to public/css/fonts.css diff --git a/webroot/css/home.css b/public/css/home.css similarity index 100% rename from webroot/css/home.css rename to public/css/home.css diff --git a/webroot/css/milligram.min.css b/public/css/milligram.min.css similarity index 100% rename from webroot/css/milligram.min.css rename to public/css/milligram.min.css diff --git a/webroot/css/normalize.min.css b/public/css/normalize.min.css similarity index 100% rename from webroot/css/normalize.min.css rename to public/css/normalize.min.css diff --git a/webroot/favicon.ico b/public/favicon.ico similarity index 100% rename from webroot/favicon.ico rename to public/favicon.ico diff --git a/webroot/font/Raleway-License.txt b/public/font/Raleway-License.txt similarity index 100% rename from webroot/font/Raleway-License.txt rename to public/font/Raleway-License.txt diff --git a/webroot/font/cakedingbats-webfont.eot b/public/font/cakedingbats-webfont.eot similarity index 100% rename from webroot/font/cakedingbats-webfont.eot rename to public/font/cakedingbats-webfont.eot diff --git a/webroot/font/cakedingbats-webfont.svg b/public/font/cakedingbats-webfont.svg similarity index 100% rename from webroot/font/cakedingbats-webfont.svg rename to public/font/cakedingbats-webfont.svg diff --git a/webroot/font/cakedingbats-webfont.ttf b/public/font/cakedingbats-webfont.ttf similarity index 100% rename from webroot/font/cakedingbats-webfont.ttf rename to public/font/cakedingbats-webfont.ttf diff --git a/webroot/font/cakedingbats-webfont.woff b/public/font/cakedingbats-webfont.woff similarity index 100% rename from webroot/font/cakedingbats-webfont.woff rename to public/font/cakedingbats-webfont.woff diff --git a/webroot/font/cakedingbats-webfont.woff2 b/public/font/cakedingbats-webfont.woff2 similarity index 100% rename from webroot/font/cakedingbats-webfont.woff2 rename to public/font/cakedingbats-webfont.woff2 diff --git a/webroot/font/raleway-400-cyrillic-ext.woff2 b/public/font/raleway-400-cyrillic-ext.woff2 similarity index 100% rename from webroot/font/raleway-400-cyrillic-ext.woff2 rename to public/font/raleway-400-cyrillic-ext.woff2 diff --git a/webroot/font/raleway-400-cyrillic.woff2 b/public/font/raleway-400-cyrillic.woff2 similarity index 100% rename from webroot/font/raleway-400-cyrillic.woff2 rename to public/font/raleway-400-cyrillic.woff2 diff --git a/webroot/font/raleway-400-latin-ext.woff2 b/public/font/raleway-400-latin-ext.woff2 similarity index 100% rename from webroot/font/raleway-400-latin-ext.woff2 rename to public/font/raleway-400-latin-ext.woff2 diff --git a/webroot/font/raleway-400-latin.woff2 b/public/font/raleway-400-latin.woff2 similarity index 100% rename from webroot/font/raleway-400-latin.woff2 rename to public/font/raleway-400-latin.woff2 diff --git a/webroot/font/raleway-400-vietnamese.woff2 b/public/font/raleway-400-vietnamese.woff2 similarity index 100% rename from webroot/font/raleway-400-vietnamese.woff2 rename to public/font/raleway-400-vietnamese.woff2 diff --git a/webroot/font/raleway-700-cyrillic-ext.woff2 b/public/font/raleway-700-cyrillic-ext.woff2 similarity index 100% rename from webroot/font/raleway-700-cyrillic-ext.woff2 rename to public/font/raleway-700-cyrillic-ext.woff2 diff --git a/webroot/font/raleway-700-cyrillic.woff2 b/public/font/raleway-700-cyrillic.woff2 similarity index 100% rename from webroot/font/raleway-700-cyrillic.woff2 rename to public/font/raleway-700-cyrillic.woff2 diff --git a/webroot/font/raleway-700-latin-ext.woff2 b/public/font/raleway-700-latin-ext.woff2 similarity index 100% rename from webroot/font/raleway-700-latin-ext.woff2 rename to public/font/raleway-700-latin-ext.woff2 diff --git a/webroot/font/raleway-700-latin.woff2 b/public/font/raleway-700-latin.woff2 similarity index 100% rename from webroot/font/raleway-700-latin.woff2 rename to public/font/raleway-700-latin.woff2 diff --git a/webroot/font/raleway-700-vietnamese.woff2 b/public/font/raleway-700-vietnamese.woff2 similarity index 100% rename from webroot/font/raleway-700-vietnamese.woff2 rename to public/font/raleway-700-vietnamese.woff2 diff --git a/webroot/img/cake-logo.png b/public/img/cake-logo.png similarity index 100% rename from webroot/img/cake-logo.png rename to public/img/cake-logo.png diff --git a/webroot/img/cake.icon.png b/public/img/cake.icon.png similarity index 100% rename from webroot/img/cake.icon.png rename to public/img/cake.icon.png diff --git a/webroot/img/cake.logo.svg b/public/img/cake.logo.svg similarity index 100% rename from webroot/img/cake.logo.svg rename to public/img/cake.logo.svg diff --git a/webroot/img/cake.power.gif b/public/img/cake.power.gif similarity index 100% rename from webroot/img/cake.power.gif rename to public/img/cake.power.gif diff --git a/webroot/index.php b/public/index.php similarity index 100% rename from webroot/index.php rename to public/index.php diff --git a/webroot/js/.gitkeep b/public/js/.gitkeep similarity index 100% rename from webroot/js/.gitkeep rename to public/js/.gitkeep diff --git a/src/Application.php b/src/Application.php index 619028491b..e61282929a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -17,8 +17,8 @@ namespace App; use App\Middleware\HostHeaderMiddleware; +use Cake\Container\ContainerInterface; use Cake\Core\Configure; -use Cake\Core\ContainerInterface; use Cake\Datasource\FactoryLocator; use Cake\Error\Middleware\ErrorHandlerMiddleware; use Cake\Event\EventManagerInterface; @@ -29,6 +29,7 @@ use Cake\ORM\Locator\TableLocator; use Cake\Routing\Middleware\AssetMiddleware; use Cake\Routing\Middleware\RoutingMiddleware; +use Cake\Routing\RouteBuilder; /** * Application setup class. @@ -86,11 +87,11 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue // Parse various types of encoded request bodies so that they are // available as array through $request->getData() - // https://book.cakephp.org/5/en/controllers/middleware.html#body-parser-middleware + // https://book.cakephp.org/6.x/controllers/middleware.html#body-parser-middleware ->add(new BodyParserMiddleware()) // Cross Site Request Forgery (CSRF) Protection Middleware - // https://book.cakephp.org/5/en/security/csrf.html#cross-site-request-forgery-csrf-middleware + // https://book.cakephp.org/6.x/security/csrf.html#cross-site-request-forgery-csrf-middleware ->add(new CsrfProtectionMiddleware([ 'httponly' => true, ])); @@ -98,12 +99,27 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue return $middlewareQueue; } + /** + * Connect routes declared on controller actions using PHP attributes. + * + * @param \Cake\Routing\RouteBuilder $routes The route builder. + * @return void + * @link https://book.cakephp.org/6.x/development/attribute-routing.html + */ + public function routes(RouteBuilder $routes): void + { + parent::routes($routes); + $routes->connectAttributes(); + + // Additional explicit routes and route-scoped middleware can be configured here. + } + /** * Register application container services. * - * @param \Cake\Core\ContainerInterface $container The Container to update. + * @param \Cake\Container\ContainerInterface $container The Container to update. * @return void - * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection + * @link https://book.cakephp.org/6.x/development/dependency-injection.html#dependency-injection */ public function services(ContainerInterface $container): void { @@ -116,7 +132,7 @@ public function services(ContainerInterface $container): void * * @param \Cake\Event\EventManagerInterface $eventManager * @return \Cake\Event\EventManagerInterface - * @link https://book.cakephp.org/5/en/core-libraries/events.html#registering-listeners + * @link https://book.cakephp.org/6.x/core-libraries/events.html#registering-listeners */ public function events(EventManagerInterface $eventManager): EventManagerInterface { diff --git a/src/Console/Installer.php b/src/Console/Installer.php index 6ef3c2dd84..a8f8660042 100644 --- a/src/Console/Installer.php +++ b/src/Console/Installer.php @@ -41,6 +41,7 @@ class Installer 'logs', 'tmp', 'tmp/cache', + 'tmp/cache/attributes', 'tmp/cache/models', 'tmp/cache/persistent', 'tmp/cache/views', diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 1b427f01f7..32e39f2b63 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -17,6 +17,8 @@ namespace App\Controller; use Cake\Controller\Controller; +use Cake\Routing\Attribute\RouteClass; +use Cake\Routing\Route\DashedRoute; /** * Application Controller @@ -24,8 +26,9 @@ * Add your application-wide methods in the class below, your controllers * will inherit them. * - * @link https://book.cakephp.org/5/en/controllers.html#the-app-controller + * @link https://book.cakephp.org/6.x/controllers.html#the-app-controller */ +#[RouteClass(DashedRoute::class)] class AppController extends Controller { /** @@ -45,7 +48,7 @@ public function initialize(): void /* * Enable the following component for recommended CakePHP form protection settings. - * see https://book.cakephp.org/5/en/controllers/components/form-protection.html + * see https://book.cakephp.org/6.x/controllers/components/form-protection.html */ //$this->loadComponent('FormProtection'); } diff --git a/src/Controller/PagesController.php b/src/Controller/PagesController.php index 99247c1a35..3b17f780e5 100644 --- a/src/Controller/PagesController.php +++ b/src/Controller/PagesController.php @@ -20,6 +20,7 @@ use Cake\Http\Exception\ForbiddenException; use Cake\Http\Exception\NotFoundException; use Cake\Http\Response; +use Cake\Routing\Attribute\Get; use Cake\View\Exception\MissingTemplateException; /** @@ -27,7 +28,7 @@ * * This controller will render views from templates/Pages/ * - * @link https://book.cakephp.org/5/en/controllers/pages-controller.html + * @link https://book.cakephp.org/6.x/controllers/pages-controller.html */ class PagesController extends AppController { @@ -43,6 +44,8 @@ class PagesController extends AppController * be found and not in debug mode. * @throws \Cake\View\Exception\MissingTemplateException In debug mode. */ + #[Get('/', name: 'home', defaults: ['path' => 'home'], pass: ['path'])] + #[Get('/pages/*', name: 'pages')] public function display(string ...$path): ?Response { if (!$path) { diff --git a/src/View/AppView.php b/src/View/AppView.php index 1bfd5dc18a..be98d626eb 100644 --- a/src/View/AppView.php +++ b/src/View/AppView.php @@ -22,7 +22,7 @@ * * Your application's default view class * - * @link https://book.cakephp.org/5/en/views.html#the-app-view + * @link https://book.cakephp.org/6.x/views.html#the-app-view */ class AppView extends View { diff --git a/templates/Pages/home.php b/templates/Pages/home.php index 710ad2f03d..3eb308333d 100644 --- a/templates/Pages/home.php +++ b/templates/Pages/home.php @@ -81,7 +81,7 @@ CakePHP

- Welcome to CakePHP Chiffon (🍰) + Welcome to CakePHP (🍰)

@@ -97,8 +97,8 @@ @@ -109,10 +109,10 @@

Environment

diff --git a/tests/TestApp/Application.php b/tests/TestApp/Application.php new file mode 100644 index 0000000000..46f5ca391b --- /dev/null +++ b/tests/TestApp/Application.php @@ -0,0 +1,23 @@ +post('/csrf-test', ['controller' => 'Pages', 'action' => 'display', 'home']); + } +} diff --git a/tests/TestCase/ApplicationTest.php b/tests/TestCase/ApplicationTest.php index 63bc3d1bbb..8715affdb8 100644 --- a/tests/TestCase/ApplicationTest.php +++ b/tests/TestCase/ApplicationTest.php @@ -23,6 +23,7 @@ use Cake\Http\MiddlewareQueue; use Cake\Routing\Middleware\AssetMiddleware; use Cake\Routing\Middleware\RoutingMiddleware; +use Cake\Routing\Router; use Cake\TestSuite\IntegrationTestTrait; use Cake\TestSuite\TestCase; @@ -85,4 +86,56 @@ public function testMiddleware() $middleware->seek(3); $this->assertInstanceOf(RoutingMiddleware::class, $middleware->current()); } + + /** + * Optional route files load alongside the controller attributes. + * + * @return void + */ + public function testOptionalRouteConfiguration(): void + { + $configDir = TMP . 'route-configuration' . DS; + mkdir($configDir); + $routesFile = $configDir . 'routes.php'; + file_put_contents($routesFile, <<<'PHP' +get('/custom', ['controller' => 'Pages', 'action' => 'display', 'custom'], 'custom'); +}; +PHP); + + try { + $application = new Application($configDir); + $application->routes(Router::createRouteBuilder('/')); + + $this->assertSame('/custom', Router::url(['_name' => 'custom'])); + $this->assertSame('/', Router::url(['_name' => 'home'])); + } finally { + unlink($routesFile); + rmdir($configDir); + } + } + + /** + * Controller attributes work without a routes configuration file. + * + * @return void + */ + public function testRoutingWithoutConfigurationFile(): void + { + $configDir = TMP . 'route-configuration' . DS; + mkdir($configDir); + + try { + $application = new Application($configDir); + $application->routes(Router::createRouteBuilder('/')); + + $this->assertSame('/', Router::url(['_name' => 'home'])); + $this->assertSame('/pages/home', Router::url(['_name' => 'pages', 'home'])); + } finally { + rmdir($configDir); + } + } } diff --git a/tests/TestCase/Controller/PagesControllerTest.php b/tests/TestCase/Controller/PagesControllerTest.php index f0f72c9d09..91f48c0c16 100644 --- a/tests/TestCase/Controller/PagesControllerTest.php +++ b/tests/TestCase/Controller/PagesControllerTest.php @@ -16,8 +16,10 @@ */ namespace App\Test\TestCase\Controller; +use App\Test\TestApp\Application; use Cake\Core\Configure; -use Cake\TestSuite\Constraint\Response\StatusCode; +use Cake\Routing\Route\DashedRoute; +use Cake\Routing\Router; use Cake\TestSuite\IntegrationTestTrait; use Cake\TestSuite\TestCase; @@ -33,7 +35,7 @@ class PagesControllerTest extends TestCase * * @return void */ - public function testDisplay() + public function testDisplay(): void { Configure::write('debug', true); $this->get('/pages/home'); @@ -42,12 +44,56 @@ public function testDisplay() $this->assertResponseContains(''); } + /** + * Test the named attribute route for the home page. + * + * @return void + */ + public function testHome(): void + { + Configure::write('debug', true); + $this->get('/'); + $this->assertResponseOk(); + $this->assertResponseContains('CakePHP'); + $this->assertResponseContains('8.5.0 or higher'); + $this->assertSame('/', Router::url(['_name' => 'home'])); + $this->assertSame('/pages/home', Router::url(['_name' => 'pages', 'home'])); + $routes = Router::getRouteCollection()->named(); + $this->assertInstanceOf(DashedRoute::class, $routes['home']); + $this->assertInstanceOf(DashedRoute::class, $routes['pages']); + } + + /** + * Test that fallback URLs are not connected. + * + * @return void + */ + public function testNoFallbackRoutes(): void + { + Configure::write('debug', true); + $this->get('/unrouted/index'); + $this->assertResponseCode(404); + $this->assertResponseContains('Missing Route'); + } + + /** + * Test that static pages do not accept POST requests, even with a CSRF token. + * + * @return void + */ + public function testPostNotRouted(): void + { + $this->enableCsrfToken(); + $this->post('/pages/home'); + $this->assertResponseCode(404); + } + /** * Test that missing template renders 404 page in production * * @return void */ - public function testMissingTemplate() + public function testMissingTemplate(): void { Configure::write('debug', false); $this->get('/pages/not_existing'); @@ -61,7 +107,7 @@ public function testMissingTemplate() * * @return void */ - public function testMissingTemplateInDebug() + public function testMissingTemplateInDebug(): void { Configure::write('debug', true); $this->get('/pages/not_existing'); @@ -72,12 +118,25 @@ public function testMissingTemplateInDebug() $this->assertResponseContains('not_existing.php'); } + /** + * Test that wildcard routes retain nested page paths. + * + * @return void + */ + public function testNestedMissingTemplateInDebug(): void + { + Configure::write('debug', true); + $this->get('/pages/nested/not_existing'); + $this->assertResponseFailure(); + $this->assertResponseContains('nested/not_existing.php'); + } + /** * Test directory traversal protection * * @return void */ - public function testDirectoryTraversalProtection() + public function testDirectoryTraversalProtection(): void { $this->get('/pages/../Layout/ajax'); $this->assertResponseCode(403); @@ -89,9 +148,10 @@ public function testDirectoryTraversalProtection() * * @return void */ - public function testCsrfAppliedError() + public function testCsrfAppliedError(): void { - $this->post('/pages/home', ['hello' => 'world']); + $this->configApplication(Application::class, [CONFIG]); + $this->post('/csrf-test', ['hello' => 'world']); $this->assertResponseCode(403); $this->assertResponseContains('CSRF'); @@ -102,12 +162,14 @@ public function testCsrfAppliedError() * * @return void */ - public function testCsrfAppliedOk() + public function testCsrfAppliedOk(): void { + Configure::write('debug', true); + $this->configApplication(Application::class, [CONFIG]); $this->enableCsrfToken(); - $this->post('/pages/home', ['hello' => 'world']); + $this->post('/csrf-test', ['hello' => 'world']); - $this->assertThat(403, $this->logicalNot(new StatusCode($this->_response))); + $this->assertResponseOk(); $this->assertResponseNotContains('CSRF'); } }